Merge pull request 'fix: resolve locked namespace binding in civilytics_load_fonts() and default theme to transparent background' (#19) from kodor/fix-18-fonts-and-theme into master
R-CMD-check / R CMD check (push) Successful in 4m25s

Reviewed-on: #19
This commit was merged in pull request #19.
This commit is contained in:
2026-08-03 10:46:40 -04:00
10 changed files with 85 additions and 55 deletions
+24 -11
View File
@@ -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_SERIF <- "Source Serif 4" # body prose / editorial long-form
CV_FONT_MONO <- "JetBrains Mono" # code, data tables, numeric callouts CV_FONT_MONO <- "JetBrains Mono" # code, data tables, numeric callouts
# Internal flag so civilytics_load_fonts() is idempotent within a session. # Mutable package state held in an environment so the binding itself stays
.cv_fonts_loaded <- FALSE # 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 #' Load Civilytics brand fonts
#' #'
#' Downloads Inter and Libre Franklin from Google Fonts via #' Downloads Inter, Libre Franklin, Source Serif 4 and JetBrains Mono from
#' [sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so #' Google Fonts via [sysfonts::font_add_google()], then calls
#' that all graphics devices render text with those fonts. This is called #' [showtext::showtext_auto()] so that all graphics devices render text with
#' automatically when the package loads; use this function to retry if the #' those fonts. This is called automatically when the package loads; use this
#' initial load failed (e.g., the machine was offline at load time). #' 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 #' @export
#' #'
#' @examples #' @examples
#' \dontrun{ #' \dontrun{
#' civilytics_load_fonts() #' 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("Inter", family = "Inter")
sysfonts::font_add_google("Libre Franklin", family = "Libre Franklin") sysfonts::font_add_google("Libre Franklin", family = "Libre Franklin")
sysfonts::font_add_google("Source Serif 4", family = "Source Serif 4") sysfonts::font_add_google("Source Serif 4", family = "Source Serif 4")
sysfonts::font_add_google("JetBrains Mono", family = "JetBrains Mono") sysfonts::font_add_google("JetBrains Mono", family = "JetBrains Mono")
showtext::showtext_auto() showtext::showtext_auto()
.cv_fonts_loaded <<- TRUE
invisible(NULL) .cv_state$fonts_loaded <- TRUE
invisible(TRUE)
} }
.onLoad <- function(libname, pkgname) { .onLoad <- function(libname, pkgname) {
+10 -8
View File
@@ -36,10 +36,12 @@
#' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`). #' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).
#' @param grid Character. Which major gridlines to draw: `"y"` (default, #' @param grid Character. Which major gridlines to draw: `"y"` (default,
#' horizontal only), `"x"` (vertical only), `"both"`, or `"none"`. #' horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.
#' @param paper_bg Logical. If `TRUE` (default), fill the plot and panel #' @param paper_bg Logical. If `TRUE`, fill the plot and panel backgrounds
#' backgrounds with the warm `paper` color. Set to `FALSE` for a #' with the warm `paper` color (Civilytics cream). Default is `FALSE`
#' transparent background (useful for slides or overlay on colored #' (transparent) so that figures composite cleanly onto any background.
#' surfaces). #' 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: #' @section Font size hierarchy:
#' All text sizes are derived from `font_size` using relative scale factors. #' All text sizes are derived from `font_size` using relative scale factors.
@@ -68,7 +70,7 @@
#' \dontrun{ #' \dontrun{
#' library(ggplot2) #' library(ggplot2)
#' #'
#' # Default editorial theme #' # Default — transparent background for embedding
#' ggplot(mpg, aes(displ, hwy)) + #' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() + #' geom_point() +
#' theme_civilytics() #' theme_civilytics()
@@ -79,10 +81,10 @@
#' scale_color_civilytics() + #' scale_color_civilytics() +
#' theme_civilytics(grid = "both") #' theme_civilytics(grid = "both")
#' #'
#' # Transparent background for embedding #' # Branded cream background (opt-in)
#' ggplot(mpg, aes(displ, hwy)) + #' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() + #' geom_point() +
#' theme_civilytics(paper_bg = FALSE) #' theme_civilytics(paper_bg = TRUE)
#' #'
#' # Larger text for poster or display #' # Larger text for poster or display
#' ggplot(mpg, aes(displ, hwy)) + #' ggplot(mpg, aes(displ, hwy)) +
@@ -102,7 +104,7 @@ theme_civilytics <- function(
accent = unname(civilytics_colors["ember_600"]), accent = unname(civilytics_colors["ember_600"]),
strip_color = unname(civilytics_colors["paper_2"]), strip_color = unname(civilytics_colors["paper_2"]),
grid = c("y", "x", "both", "none"), grid = c("y", "x", "both", "none"),
paper_bg = TRUE) { paper_bg = FALSE) {
grid <- match.arg(grid) grid <- match.arg(grid)
half_line <- font_size / 2 half_line <- font_size / 2
+15 -7
View File
@@ -4,17 +4,25 @@
\alias{civilytics_load_fonts} \alias{civilytics_load_fonts}
\title{Load Civilytics brand fonts} \title{Load Civilytics brand fonts}
\usage{ \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{ \value{
Invisibly returns `NULL`. Invisibly returns \code{TRUE} if fonts were loaded, \code{FALSE} if skipped
(already loaded and \code{force = FALSE}).
} }
\description{ \description{
Downloads Inter and Libre Franklin from Google Fonts via Downloads Inter, Libre Franklin, Source Serif 4 and JetBrains Mono from
[sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so Google Fonts via \code{sysfonts::font_add_google()}, then calls
that all graphics devices render text with those fonts. This is called \code{showtext::showtext_auto()} so that all graphics devices render text with
automatically when the package loads; use this function to retry if the those fonts. This is called automatically when the package loads; use this
initial load failed (e.g., the machine was offline at load time). 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{ \examples{
\dontrun{ \dontrun{
+10 -8
View File
@@ -17,7 +17,7 @@ theme_civilytics(
accent = unname(civilytics_colors["ember_600"]), accent = unname(civilytics_colors["ember_600"]),
strip_color = unname(civilytics_colors["paper_2"]), strip_color = unname(civilytics_colors["paper_2"]),
grid = c("y", "x", "both", "none"), grid = c("y", "x", "both", "none"),
paper_bg = TRUE paper_bg = FALSE
) )
} }
\arguments{ \arguments{
@@ -56,10 +56,12 @@ to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{grid}{Character. Which major gridlines to draw: `"y"` (default, \item{grid}{Character. Which major gridlines to draw: `"y"` (default,
horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.} horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} 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{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
@@ -105,7 +107,7 @@ shrinkage.
\dontrun{ \dontrun{
library(ggplot2) library(ggplot2)
# Default editorial theme # Default — transparent background for embedding
ggplot(mpg, aes(displ, hwy)) + ggplot(mpg, aes(displ, hwy)) +
geom_point() + geom_point() +
theme_civilytics() theme_civilytics()
@@ -116,10 +118,10 @@ ggplot(mpg, aes(displ, hwy, colour = class)) +
scale_color_civilytics() + scale_color_civilytics() +
theme_civilytics(grid = "both") theme_civilytics(grid = "both")
# Transparent background for embedding # Branded cream background (opt-in)
ggplot(mpg, aes(displ, hwy)) + ggplot(mpg, aes(displ, hwy)) +
geom_point() + geom_point() +
theme_civilytics(paper_bg = FALSE) theme_civilytics(paper_bg = TRUE)
# Larger text for poster or display # Larger text for poster or display
ggplot(mpg, aes(displ, hwy)) + ggplot(mpg, aes(displ, hwy)) +
+4 -4
View File
@@ -56,10 +56,10 @@ to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{grid}{Character. Which major gridlines to draw: `"y"` (default, \item{grid}{Character. Which major gridlines to draw: `"y"` (default,
horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.} horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+4 -4
View File
@@ -52,10 +52,10 @@ design system.}
\item{strip_color}{Character. Hex code for facet strip background. Defaults \item{strip_color}{Character. Hex code for facet strip background. Defaults
to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+4 -4
View File
@@ -52,10 +52,10 @@ design system.}
\item{strip_color}{Character. Hex code for facet strip background. Defaults \item{strip_color}{Character. Hex code for facet strip background. Defaults
to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+4 -4
View File
@@ -56,10 +56,10 @@ to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{grid}{Character. Which major gridlines to draw: `"y"` (default, \item{grid}{Character. Which major gridlines to draw: `"y"` (default,
horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.} horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+4 -4
View File
@@ -52,10 +52,10 @@ design system.}
\item{strip_color}{Character. Hex code for facet strip background. Defaults \item{strip_color}{Character. Hex code for facet strip background. Defaults
to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+6 -1
View File
@@ -133,8 +133,13 @@ test_that("theme_civilytics uses brand ink color for text", {
expect_equal(th$text$colour, unname(civilytics_colors["ink"])) 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() 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"])) expect_equal(th$plot.background$fill, unname(civilytics_colors["paper"]))
}) })