feat: expand color system to 53 colors/10 palettes, add grid/slide themes, and README gallery
R-CMD-check / R CMD check (push) Successful in 3m41s

Replace the 16-color / 3-palette system with the full Civilytics design
system: 53 named colors (full navy, ember, violet ramps plus teal, plum,
moss, brass supporting hues) and 10 visualization palettes (qual,
qual_warm, qual_cool, seq_ember, seq_navy, seq_violet, seq_paper_ink,
div_navy_ember, div_violet_ember).

Enrich theme_civilytics() with grid and paper_bg parameters, add
theme_civilytics_slide() for presentations, and load all four brand fonts
(Inter, Libre Franklin, Source Serif 4, JetBrains Mono).

Add README.Rmd with rendered gallery showing all palettes, theme
variants, grid options, dark/slide modes, facets, and scale usage.
This commit is contained in:
2026-05-19 10:35:32 -06:00
parent 099fff1d1d
commit 515bae8685
29 changed files with 1209 additions and 273 deletions
+6 -4
View File
@@ -1,13 +1,15 @@
Package: civilytics Package: civilytics
Type: Package Type: Package
Title: Utilities Functions for Civilytics Title: Brand Themes, Color Palettes, and Utility Functions for Civilytics
Version: 0.2.0 Version: 0.2.0
Authors@R: Authors@R:
person("Jared", "E. Knowles", email = "jared@civilytics.com", person("Jared", "E. Knowles", email = "jared@civilytics.com",
role = c("aut", "cre")) role = c("aut", "cre"))
Description: House R functions for Civilytics Consulting LLC. This package Description: Provides a complete ggplot2 brand theme system for Civilytics
implements a variety of useful functions for creating and branding analyses Consulting LLC, including editorial light, dark, and slide-optimized themes;
produced by Civilytics Consulting LLC. 10 curated color palettes for qualitative, sequential, and diverging data;
logo composition utilities; and data-wrangling helpers for public-sector
analysis.
License: LGPL (>= 3) License: LGPL (>= 3)
URL: https://gitea.civilytics.org/Civilytics/civilyticsR URL: https://gitea.civilytics.org/Civilytics/civilyticsR
BugReports: https://gitea.civilytics.org/Civilytics/civilyticsR/issues BugReports: https://gitea.civilytics.org/Civilytics/civilyticsR/issues
+3
View File
@@ -7,6 +7,8 @@ export(civilytics_colors)
export(civilytics_load_fonts) export(civilytics_load_fonts)
export(civilytics_logo) export(civilytics_logo)
export(civilytics_pal) export(civilytics_pal)
export(civilytics_palette)
export(civilytics_palettes)
export(clopper_pearson) export(clopper_pearson)
export(countCleanr) export(countCleanr)
export(countDots) export(countDots)
@@ -42,6 +44,7 @@ export(simpleCap)
export(star_subs) export(star_subs)
export(theme_civilytics) export(theme_civilytics)
export(theme_civilytics_dark) export(theme_civilytics_dark)
export(theme_civilytics_slide)
export(trim_max) export(trim_max)
export(waldInterval) export(waldInterval)
export(z_gap_test) export(z_gap_test)
+33 -14
View File
@@ -1,14 +1,33 @@
#' @keywords internal #' @description
#' @importFrom stats qbeta #' Provides a complete ggplot2 brand theme system for Civilytics Consulting,
#' @importFrom stats qnorm #' including editorial light, dark, and slide-optimized themes; 10 curated
"_PACKAGE" #' color palettes; logo composition; and data-wrangling helpers for
#' public-sector analysis.
## usethis namespace: start #'
## usethis namespace: end #' @section Themes:
NULL #' \itemize{
#' \item [theme_civilytics()] -- editorial theme with warm paper background
# state.abb and state.name are lazy data from the datasets package (base R). #' \item [theme_civilytics_dark()] -- navy background variant
# They cannot be imported via @importFrom — suppress the R CMD check NOTE here. #' \item [theme_civilytics_slide()] -- transparent background, larger text
utils::globalVariables(c("state.abb", "state.name")) #' }
#'
#' @section Color palettes:
#' \itemize{
#' \item [civilytics_colors] -- 53 named brand colors
#' \item [civilytics_palettes] -- 10 visualization palettes
#' \item [civilytics_palette()] -- extract colors by palette name
#' \item [scale_color_civilytics()] / [scale_fill_civilytics()] -- ggplot2 scales
#' }
#'
#' @keywords internal
#' @importFrom stats qbeta
#' @importFrom stats qnorm
"_PACKAGE"
## usethis namespace: start
## usethis namespace: end
NULL
# state.abb and state.name are lazy data from the datasets package (base R).
# They cannot be imported via @importFrom -- suppress the R CMD check NOTE here.
utils::globalVariables(c("state.abb", "state.name"))
+216 -83
View File
@@ -1,106 +1,229 @@
#' Civilytics brand colors #' Civilytics brand colors
#' #'
#' A named character vector of all Civilytics brand colors, matching the CSS #' A named character vector of all Civilytics brand colors, matching the CSS
#' custom properties defined on www.civilytics.com (`--cv-*` variables). #' custom properties in the Civilytics design system (`--cv-*` variables).
#' Includes full ramps for navy, ember, and violet, plus supporting hues
#' (teal, plum, moss, brass) and semantic status colors.
#' #'
#' @format A named character vector of hex color codes. #' @format A named character vector of hex color codes.
#' @export #' @export
#' #'
#' @examples #' @examples
#' civilytics_colors["accent"] #' civilytics_colors["ink"]
#' civilytics_colors[c("ink", "paper")] #' civilytics_colors[c("navy_600", "ember_600", "teal_600")]
civilytics_colors <- c( civilytics_colors <- c(
# Paper (backgrounds) — warm off-white scale # Neutrals — warm paper -> civic ink
paper = "#FAF7F2", paper = "#FAF7F2",
paper_2 = "#F2EDE4", paper_2 = "#F2EDE4",
paper_3 = "#E6DFD1", paper_3 = "#E6DFD1",
# Rules (borders/dividers)
rule = "#D6CEBD", rule = "#D6CEBD",
rule_strong = "#B8AE97", rule_strong = "#B8AE97",
# Ink (text / foreground) — dark-to-light navy-grey scale
ink = "#0E1A2B", ink = "#0E1A2B",
ink_2 = "#2B3A52", ink_2 = "#2B3A52",
ink_3 = "#5A6A82", ink_3 = "#5A6A82",
ink_4 = "#8C97AB", ink_4 = "#8C97AB",
# Navy — primary brand blue
navy = "#22406A", # Navy — civic authority (primary brand blue)
navy_dark = "#1A2E4A", navy_900 = "#0E1A2B", navy_800 = "#132339", navy_700 = "#1A2E4A",
# Accent — burnt orange navy_600 = "#22406A", navy_500 = "#2E5590", navy_400 = "#4A74B0",
accent = "#C25311", navy_300 = "#7A9BCA", navy_200 = "#B3C6E0", navy_100 = "#DDE6F2",
accent_dark = "#E07840", navy_50 = "#EEF3FA",
accent_50 = "#FDF1E4",
accent_100 = "#FBE0C6", # Ember — warm orange accent
accent_200 = "#F8C8A3" ember_900 = "#451A00", ember_800 = "#6B2B00", ember_700 = "#923D00",
ember_600 = "#C25311", ember_500 = "#DB6C25", ember_400 = "#EA8A49",
ember_300 = "#F2A976", ember_200 = "#F8C8A3", ember_100 = "#FBE0C6",
ember_50 = "#FDF1E4",
# Violet — extended supporting
violet_900 = "#19102E", violet_800 = "#2A1B4D", violet_700 = "#3D2A6B",
violet_600 = "#5C3A8A", violet_500 = "#7556A8", violet_400 = "#9A7EC2",
violet_300 = "#BDA8DA", violet_200 = "#DCD0EC", violet_100 = "#F1ECF8",
# Supporting — muted editorial hues for data viz
teal_600 = "#1F6F70", teal_300 = "#7CB3B3", teal_100 = "#D3E6E6",
plum_600 = "#6B3A5E", plum_300 = "#B392A7", plum_100 = "#E7DAE1",
moss_600 = "#4A6B2F", moss_300 = "#9CB47D", moss_100 = "#DEE8CF",
brass_600 = "#B8751C", brass_100 = "#F9E6C8",
# Semantic status colors
success = "#4A6B2F",
warning = "#9A5F18",
danger = "#A6271D",
info = "#2E5590"
) )
# Internal named list of visualization palettes.
# Each entry is a character vector of hex codes ordered for visual distinction. #' Civilytics visualization palettes
.cv_palettes <- list( #'
# Qualitative: distinct hues for categorical data (up to 6 categories) #' Named list of curated color palettes for data visualization. Qualitative
main = unname(civilytics_colors[c( #' palettes use distinct hues for categorical data; sequential palettes ramp
"navy_dark", "accent", "ink_3", "rule_strong", "navy", "accent_200" #' through a single hue for ordered data; diverging palettes fan out from a
)]), #' neutral midpoint.
# Sequential: light-to-dark navy for ordered/continuous data #'
sequential = unname(civilytics_colors[c( #' @format A named list of character vectors of hex color codes.
"paper", "ink_4", "ink_3", "navy", "navy_dark" #'
)]), #' @section Qualitative (categorical data):
# Diverging: orange <-> neutral <-> navy for data with a meaningful midpoint #' \describe{
diverging = unname(civilytics_colors[c( #' \item{`qual`}{7 distinct hues: navy, ember, plum, violet, red, teal, ink-3}
"accent", "accent_200", "paper", "ink_4", "navy_dark" #' \item{`qual_warm`}{Warm-leaning: ember, red, magenta, plum, violet}
)]) #' \item{`qual_cool`}{Cool-leaning: navy, violet, plum, teal}
#' }
#'
#' @section Sequential (ordered/continuous data):
#' \describe{
#' \item{`seq_ember`}{Light to dark ember (9 stops)}
#' \item{`seq_navy`}{Light to dark navy (9 stops)}
#' \item{`seq_violet`}{Light to dark violet (9 stops)}
#' \item{`seq_paper_ink`}{Paper through ember/plum to ink (8 stops, good for heatmaps)}
#' }
#'
#' @section Diverging (data with a meaningful midpoint):
#' \describe{
#' \item{`div_navy_ember`}{Navy <-> paper <-> ember (9 stops)}
#' \item{`div_violet_ember`}{Violet <-> paper <-> ember (9 stops)}
#' }
#'
#' @export
#'
#' @examples
#' names(civilytics_palettes)
#' civilytics_palettes[["qual"]]
civilytics_palettes <- list(
# -- Qualitative --
qual = c(
"#22406A",
"#C25311",
"#6B3A5E",
"#3D2A6B",
"#A6271D",
"#1F6F70",
"#5A6A82"
),
qual_warm = c(
"#C25311",
"#A6271D",
"#923D00",
"#B8366B",
"#6B3A5E",
"#DB6C25",
"#3D2A6B"
),
qual_cool = c(
"#22406A",
"#3D2A6B",
"#6B3A5E",
"#1F6F70",
"#2E5590",
"#7556A8",
"#5A6A82"
),
# -- Sequential --
seq_ember = c(
"#FDF1E4", "#FBE0C6", "#F8C8A3", "#F2A976",
"#EA8A49", "#DB6C25", "#C25311", "#923D00", "#451A00"
),
seq_navy = c(
"#EEF3FA", "#DDE6F2", "#B3C6E0", "#7A9BCA",
"#4A74B0", "#2E5590", "#22406A", "#1A2E4A", "#0E1A2B"
),
seq_violet = c(
"#F1ECF8", "#DCD0EC", "#BDA8DA", "#9A7EC2",
"#7556A8", "#5C3A8A", "#3D2A6B", "#2A1B4D", "#19102E"
),
seq_paper_ink = c(
"#FAF7F2", "#F8C8A3", "#EA8A49", "#C25311",
"#A6271D", "#6B3A5E", "#3D2A6B", "#0E1A2B"
),
# -- Diverging --
div_navy_ember = c(
"#0E1A2B", "#22406A", "#4A74B0", "#B3C6E0",
"#FAF7F2",
"#F8C8A3", "#EA8A49", "#C25311", "#6B2B00"
),
div_violet_ember = c(
"#19102E", "#3D2A6B", "#7556A8", "#BDA8DA",
"#FAF7F2",
"#F8C8A3", "#EA8A49", "#C25311", "#6B2B00"
)
) )
# 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 #' Get colors from a Civilytics palette
#' #'
#' Returns a character vector of hex codes from a named Civilytics palette. #' Returns a character vector of hex codes from a named Civilytics palette.
#' Available palettes: `"main"` (qualitative, up to 6), `"sequential"` (light #' For qualitative palettes, colors beyond the palette length recycle with a
#' to dark navy), `"diverging"` (orange–neutral–navy). #' warning. For sequential and diverging palettes, colors are interpolated
#' via [grDevices::colorRampPalette()].
#' #'
#' @param name Character. Palette name: `"main"`, `"sequential"`, or #' @param name Character. Palette name. See `names(civilytics_palettes)`.
#' `"diverging"`. Defaults to `"main"`.
#' @param n Integer or `NULL`. Number of colors to return. If `NULL`, returns #' @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, #' all defined stops.
#' colors are interpolated via [grDevices::colorRampPalette()]. #' @param reverse Logical. Reverse the palette order. Default `FALSE`.
#' #'
#' @return A character vector of hex color codes. #' @return A character vector of hex color codes.
#' @export #' @export
#' #'
#' @examples #' @examples
#' civilytics_pal() # all 6 qualitative colors #' civilytics_palette() # all 7 qualitative colors
#' civilytics_pal("sequential", n = 3) #' civilytics_palette("seq_navy", n = 5) # 5-stop navy ramp
#' civilytics_pal("diverging", n = 5) #' civilytics_palette("div_navy_ember", n = 11, reverse = TRUE)
civilytics_pal <- function(name = "main", n = NULL) { civilytics_palette <- function(name = "qual", n = NULL, reverse = FALSE) {
f <- .cv_pal_fun(name) pal <- civilytics_palettes[[name]]
len <- length(.cv_palettes[[name]]) if (is.null(pal)) {
f(if (is.null(n)) len else n) stop(
"'", name, "' is not a valid palette. Choose from: ",
paste(names(civilytics_palettes), collapse = ", "),
call. = FALSE
)
}
if (reverse) pal <- rev(pal)
if (is.null(n)) return(pal)
if (startsWith(name, "qual")) {
if (n > length(pal)) {
warning(
"Requested ", n, " colors from '", name, "' palette (max ",
length(pal), "); recycling.",
call. = FALSE
)
pal <- rep_len(pal, n)
}
return(pal[seq_len(n)])
}
grDevices::colorRampPalette(pal)(n)
} }
#' Civilytics palette function (closure)
#'
#' Returns a closure `function(n)` suitable for passing to
#' [ggplot2::discrete_scale()] or similar scale constructors.
#'
#' @param name Character. Palette name. See `names(civilytics_palettes)`.
#' @param reverse Logical. Reverse the palette order. Default `FALSE`.
#'
#' @return A function that takes integer `n` and returns `n` hex color codes.
#' @export
#'
#' @examples
#' pal_fn <- civilytics_pal("qual")
#' pal_fn(4)
civilytics_pal <- function(name = "qual", reverse = FALSE) {
function(n) civilytics_palette(name, n = n, reverse = reverse)
}
#' Civilytics color scale for ggplot2 #' Civilytics color scale for ggplot2
#' #'
#' Applies a Civilytics brand palette to the `colour` aesthetic. Use #' Applies a Civilytics brand palette to the `colour` aesthetic.
#' `discrete = TRUE` for categorical variables and `discrete = FALSE` for
#' continuous gradients.
#' #'
#' @param palette Character. Palette name passed to [civilytics_pal()]. #' @param palette Character. Palette name. Defaults to `"qual"`.
#' Defaults to `"main"`. #' @param discrete Logical. `TRUE` (default) for categorical data; `FALSE`
#' @param discrete Logical. `TRUE` (default) for a discrete scale; `FALSE` for #' for a continuous gradient via [ggplot2::scale_color_gradientn()].
#' a continuous gradient via [ggplot2::scale_color_gradientn()]. #' @param reverse Logical. Reverse the palette order. Default `FALSE`.
#' @param ... Additional arguments passed to the underlying ggplot2 scale #' @param ... Additional arguments passed to the ggplot2 scale function.
#' function.
#' #'
#' @return A ggplot2 scale object. #' @return A ggplot2 scale object.
#' @export #' @export
@@ -113,30 +236,34 @@ civilytics_pal <- function(name = "main", n = NULL) {
#' #'
#' ggplot(mpg, aes(displ, hwy, colour = cty)) + #' ggplot(mpg, aes(displ, hwy, colour = cty)) +
#' geom_point() + #' geom_point() +
#' scale_color_civilytics("sequential", discrete = FALSE) #' scale_color_civilytics("seq_navy", discrete = FALSE)
scale_color_civilytics <- function(palette = "main", discrete = TRUE, ...) { scale_color_civilytics <- function(palette = "qual", discrete = TRUE,
reverse = FALSE, ...) {
if (discrete) { if (discrete) {
ggplot2::discrete_scale("colour", palette = .cv_pal_fun(palette), ...) ggplot2::discrete_scale(
"colour",
palette = civilytics_pal(palette, reverse = reverse),
...
)
} else { } else {
pal <- civilytics_palette(palette, reverse = reverse)
ggplot2::scale_color_gradientn( ggplot2::scale_color_gradientn(
colours = grDevices::colorRampPalette(.cv_palettes[[palette]])(256), colours = grDevices::colorRampPalette(pal)(256),
... ...
) )
} }
} }
#' Civilytics fill scale for ggplot2 #' Civilytics fill scale for ggplot2
#' #'
#' Applies a Civilytics brand palette to the `fill` aesthetic. Use #' Applies a Civilytics brand palette to the `fill` aesthetic.
#' `discrete = TRUE` for categorical variables and `discrete = FALSE` for
#' continuous gradients.
#' #'
#' @param palette Character. Palette name passed to [civilytics_pal()]. #' @param palette Character. Palette name. Defaults to `"qual"`.
#' Defaults to `"main"`. #' @param discrete Logical. `TRUE` (default) for categorical data; `FALSE`
#' @param discrete Logical. `TRUE` (default) for a discrete scale; `FALSE` for #' for a continuous gradient via [ggplot2::scale_fill_gradientn()].
#' a continuous gradient via [ggplot2::scale_fill_gradientn()]. #' @param reverse Logical. Reverse the palette order. Default `FALSE`.
#' @param ... Additional arguments passed to the underlying ggplot2 scale #' @param ... Additional arguments passed to the ggplot2 scale function.
#' function.
#' #'
#' @return A ggplot2 scale object. #' @return A ggplot2 scale object.
#' @export #' @export
@@ -149,13 +276,19 @@ scale_color_civilytics <- function(palette = "main", discrete = TRUE, ...) {
#' #'
#' ggplot(faithfuld, aes(waiting, eruptions, fill = density)) + #' ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
#' geom_tile() + #' geom_tile() +
#' scale_fill_civilytics("sequential", discrete = FALSE) #' scale_fill_civilytics("seq_ember", discrete = FALSE)
scale_fill_civilytics <- function(palette = "main", discrete = TRUE, ...) { scale_fill_civilytics <- function(palette = "qual", discrete = TRUE,
reverse = FALSE, ...) {
if (discrete) { if (discrete) {
ggplot2::discrete_scale("fill", palette = .cv_pal_fun(palette), ...) ggplot2::discrete_scale(
"fill",
palette = civilytics_pal(palette, reverse = reverse),
...
)
} else { } else {
pal <- civilytics_palette(palette, reverse = reverse)
ggplot2::scale_fill_gradientn( ggplot2::scale_fill_gradientn(
colours = grDevices::colorRampPalette(.cv_palettes[[palette]])(256), colours = grDevices::colorRampPalette(pal)(256),
... ...
) )
} }
+7 -3
View File
@@ -2,7 +2,8 @@
# These match the --cv-font-* CSS custom properties on www.civilytics.com. # These match the --cv-font-* CSS custom properties on www.civilytics.com.
CV_FONT_DISPLAY <- "Libre Franklin" # headings / display text CV_FONT_DISPLAY <- "Libre Franklin" # headings / display text
CV_FONT_SANS <- "Inter" # axis text, legends, UI elements CV_FONT_SANS <- "Inter" # axis text, legends, UI elements
CV_FONT_SERIF <- "Source Serif 4" # body prose (not used in default theme) 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. # Internal flag so civilytics_load_fonts() is idempotent within a session.
.cv_fonts_loaded <- FALSE .cv_fonts_loaded <- FALSE
@@ -25,6 +26,8 @@ CV_FONT_SERIF <- "Source Serif 4" # body prose (not used in default theme)
civilytics_load_fonts <- function() { civilytics_load_fonts <- function() {
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("JetBrains Mono", family = "JetBrains Mono")
showtext::showtext_auto() showtext::showtext_auto()
.cv_fonts_loaded <<- TRUE .cv_fonts_loaded <<- TRUE
invisible(NULL) invisible(NULL)
@@ -43,8 +46,9 @@ civilytics_load_fonts <- function() {
.onAttach <- function(libname, pkgname) { .onAttach <- function(libname, pkgname) {
if (isTRUE(getOption(".civilytics_fonts_failed"))) { if (isTRUE(getOption(".civilytics_fonts_failed"))) {
packageStartupMessage( packageStartupMessage(
"[civilytics] Brand fonts (Inter, Libre Franklin) could not be loaded ", "[civilytics] Brand fonts (Inter, Libre Franklin, Source Serif 4, ",
"from Google Fonts. Charts will fall back to system fonts. ", "JetBrains Mono) could not be loaded from Google Fonts. ",
"Charts will fall back to system fonts. ",
"Call civilytics_load_fonts() once you have an internet connection." "Call civilytics_load_fonts() once you have an internet connection."
) )
} }
+168 -58
View File
@@ -4,6 +4,11 @@
#' Civilytics brand color palette and typography. Requires ggplot2 >= 4.0.0 #' Civilytics brand color palette and typography. Requires ggplot2 >= 4.0.0
#' for the `ink`, `paper`, and `accent` base-theme parameters. #' 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 #' Brand fonts (Inter for UI text, Libre Franklin for titles) are loaded
#' automatically via [showtext] when the package is attached. Call #' automatically via [showtext] when the package is attached. Call
#' [civilytics_load_fonts()] to reload them if needed. #' [civilytics_load_fonts()] to reload them if needed.
@@ -25,9 +30,15 @@
#' @param paper Character. Hex code for background color. Defaults to #' @param paper Character. Hex code for background color. Defaults to
#' [civilytics_colors]`["paper"]` (`#FAF7F2`). #' [civilytics_colors]`["paper"]` (`#FAF7F2`).
#' @param accent Character. Hex code for accent/highlight color. Defaults to #' @param accent Character. Hex code for accent/highlight color. Defaults to
#' [civilytics_colors]`["accent"]` (`#C25311`). #' [civilytics_colors]`["ember_600"]` (`#C25311`).
#' @param strip_color Character. Hex code for facet strip background. Defaults #' @param strip_color Character. Hex code for facet strip background. Defaults
#' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`). #' 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).
#' #'
#' @return A complete ggplot2 [ggplot2::theme()] object. #' @return A complete ggplot2 [ggplot2::theme()] object.
#' @export #' @export
@@ -35,14 +46,22 @@
#' @examples #' @examples
#' \dontrun{ #' \dontrun{
#' library(ggplot2) #' library(ggplot2)
#'
#' # Default editorial theme
#' ggplot(mpg, aes(displ, hwy)) + #' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() + #' geom_point() +
#' theme_civilytics() #' theme_civilytics()
#' #'
#' # With both gridlines and brand colors
#' ggplot(mpg, aes(displ, hwy, colour = class)) + #' ggplot(mpg, aes(displ, hwy, colour = class)) +
#' geom_point() + #' geom_point() +
#' scale_color_civilytics() + #' scale_color_civilytics() +
#' theme_civilytics() #' theme_civilytics(grid = "both")
#'
#' # Transparent background for embedding
#' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics(paper_bg = FALSE)
#' } #' }
theme_civilytics <- function( theme_civilytics <- function(
font_size = 14, font_size = 14,
@@ -54,11 +73,22 @@ theme_civilytics <- function(
rel_large = 16 / 14, rel_large = 16 / 14,
ink = unname(civilytics_colors["ink"]), ink = unname(civilytics_colors["ink"]),
paper = unname(civilytics_colors["paper"]), paper = unname(civilytics_colors["paper"]),
accent = unname(civilytics_colors["accent"]), 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"),
paper_bg = TRUE) {
grid <- match.arg(grid)
half_line <- font_size / 2 half_line <- font_size / 2
small_size <- rel_small * font_size 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( ggplot2::theme_grey(
base_size = font_size, base_size = font_size,
@@ -92,16 +122,16 @@ theme_civilytics <- function(
margin = ggplot2::margin(), margin = ggplot2::margin(),
debug = FALSE debug = FALSE
), ),
# Axes # -- Axes --
axis.line = ggplot2::element_line( axis.line = ggplot2::element_blank(),
axis.line.x = ggplot2::element_line(
color = ink, color = ink,
linewidth = line_size, linewidth = 0.6,
lineend = "square" lineend = "square"
), ),
axis.line.x = NULL, axis.line.y = ggplot2::element_blank(),
axis.line.y = NULL,
axis.text = ggplot2::element_text( axis.text = ggplot2::element_text(
color = ink, color = ink_2,
size = small_size size = small_size
), ),
axis.text.x = ggplot2::element_text( axis.text.x = ggplot2::element_text(
@@ -121,66 +151,80 @@ theme_civilytics <- function(
hjust = 0 hjust = 0
), ),
axis.ticks = ggplot2::element_line( axis.ticks = ggplot2::element_line(
color = ink, color = ink_3,
linewidth = line_size linewidth = 0.4
), ),
axis.ticks.length = ggplot2::unit(half_line / 2, "pt"), axis.ticks.length = ggplot2::unit(4, "pt"),
axis.title.x = ggplot2::element_text( axis.title.x = ggplot2::element_text(
margin = ggplot2::margin(t = half_line / 2), size = ggplot2::rel(rel_small),
color = ink_3,
margin = ggplot2::margin(t = 10),
vjust = 1 vjust = 1
), ),
axis.title.x.top = ggplot2::element_text( axis.title.x.top = ggplot2::element_text(
size = ggplot2::rel(rel_small),
color = ink_3,
margin = ggplot2::margin(b = half_line / 2), margin = ggplot2::margin(b = half_line / 2),
vjust = 0 vjust = 0
), ),
axis.title.y = ggplot2::element_text( axis.title.y = ggplot2::element_text(
size = ggplot2::rel(rel_small),
color = ink_3,
angle = 90, angle = 90,
margin = ggplot2::margin(r = half_line / 2), margin = ggplot2::margin(r = 10),
vjust = 1 vjust = 1
), ),
axis.title.y.right = ggplot2::element_text( axis.title.y.right = ggplot2::element_text(
size = ggplot2::rel(rel_small),
color = ink_3,
angle = -90, angle = -90,
margin = ggplot2::margin(l = half_line / 2), margin = ggplot2::margin(l = half_line / 2),
vjust = 0 vjust = 0
), ),
# Legend # -- Legend --
legend.background = ggplot2::element_blank(), legend.background = ggplot2::element_blank(),
legend.spacing = ggplot2::unit(font_size, "pt"), legend.spacing = ggplot2::unit(font_size, "pt"),
legend.spacing.x = NULL, legend.spacing.x = NULL,
legend.spacing.y = NULL, legend.spacing.y = NULL,
legend.margin = ggplot2::margin(0, 0, 0, 0), legend.margin = ggplot2::margin(0, 0, 4, 0),
legend.key = ggplot2::element_blank(), legend.key = ggplot2::element_blank(),
legend.key.size = ggplot2::unit(1.1 * font_size, "pt"), legend.key.size = ggplot2::unit(12, "pt"),
legend.key.height = NULL, legend.key.height = NULL,
legend.key.width = NULL, legend.key.width = NULL,
legend.text = ggplot2::element_text(size = ggplot2::rel(rel_small)), legend.text = ggplot2::element_text(
legend.title = ggplot2::element_text(hjust = 0), size = ggplot2::rel(rel_small),
legend.position = "right", 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.direction = NULL,
legend.justification = c("left", "center"), legend.justification = c("left", "center"),
legend.box = NULL, legend.box = NULL,
legend.box.margin = ggplot2::margin(0, 0, 0, 0), legend.box.margin = ggplot2::margin(0, 0, 0, 0),
legend.box.background = ggplot2::element_blank(), legend.box.background = ggplot2::element_blank(),
legend.box.spacing = ggplot2::unit(font_size, "pt"), legend.box.spacing = ggplot2::unit(font_size, "pt"),
# Panel # -- Panel --
panel.background = ggplot2::element_blank(), panel.background = ggplot2::element_rect(fill = bg_color, color = NA),
panel.border = ggplot2::element_blank(), panel.border = ggplot2::element_blank(),
panel.grid = ggplot2::element_blank(), panel.grid.minor = ggplot2::element_blank(),
panel.grid.major = NULL, panel.grid.major.x = if (grid %in% c("x", "both")) grid_line else no_line,
panel.grid.minor = NULL, panel.grid.major.y = if (grid %in% c("y", "both")) grid_line else no_line,
panel.grid.major.x = NULL, panel.spacing = ggplot2::unit(16, "pt"),
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.x = NULL,
panel.spacing.y = NULL, panel.spacing.y = NULL,
panel.ontop = FALSE, panel.ontop = FALSE,
# Facet strips — use brand paper_2 tint (overridden for dark variant) # -- Facet strips --
strip.background = ggplot2::element_rect(fill = strip_color), strip.background = ggplot2::element_rect(fill = strip_color, color = NA),
strip.text = ggplot2::element_text( strip.text = ggplot2::element_text(
family = title_family, family = font_family,
face = "bold",
size = ggplot2::rel(rel_small), size = ggplot2::rel(rel_small),
color = ink,
margin = ggplot2::margin( margin = ggplot2::margin(
half_line / 2, half_line / 2, half_line / 2, half_line / 2,
half_line / 2, half_line / 2 half_line / 2, half_line / 2
@@ -193,49 +237,55 @@ theme_civilytics <- function(
strip.placement.y = NULL, strip.placement.y = NULL,
strip.switch.pad.grid = ggplot2::unit(half_line / 2, "pt"), strip.switch.pad.grid = ggplot2::unit(half_line / 2, "pt"),
strip.switch.pad.wrap = ggplot2::unit(half_line / 2, "pt"), strip.switch.pad.wrap = ggplot2::unit(half_line / 2, "pt"),
# Plot-level # -- Plot-level --
plot.background = ggplot2::element_rect(fill = paper, color = NA), plot.background = ggplot2::element_rect(fill = bg_color, color = NA),
plot.title = ggplot2::element_text( plot.title = ggplot2::element_text(
family = title_family, family = title_family,
face = "bold", face = "bold",
size = ggplot2::rel(rel_large), size = ggplot2::rel(rel_large),
hjust = 0, hjust = 0,
vjust = 1, vjust = 1,
margin = ggplot2::margin(b = half_line) margin = ggplot2::margin(b = 4)
), ),
plot.title.position = "plot",
plot.subtitle = ggplot2::element_text( plot.subtitle = ggplot2::element_text(
size = ggplot2::rel(rel_small), size = ggplot2::rel(rel_small),
hjust = 0, color = ink_2,
vjust = 1, hjust = 0,
margin = ggplot2::margin(b = half_line) vjust = 1,
lineheight = 1.3,
margin = ggplot2::margin(b = 14)
), ),
plot.caption = ggplot2::element_text( plot.caption = ggplot2::element_text(
size = ggplot2::rel(rel_tiny), size = ggplot2::rel(rel_tiny),
color = ink_3,
hjust = 0, hjust = 0,
vjust = 1, vjust = 1,
lineheight = 1, lineheight = 1.3,
margin = ggplot2::margin(t = half_line) margin = ggplot2::margin(t = 14)
), ),
plot.caption.position = "plot",
plot.tag = ggplot2::element_text( plot.tag = ggplot2::element_text(
face = "bold", face = "bold",
color = accent,
size = ggplot2::rel(rel_tiny),
hjust = 0, hjust = 0,
vjust = 0.7 vjust = 0.7
), ),
plot.tag.position = c(0, 1), plot.tag.position = c(0, 1),
plot.margin = ggplot2::margin(half_line, half_line, half_line, half_line), plot.margin = ggplot2::margin(16, 18, 16, 16),
complete = TRUE complete = TRUE
) )
} }
#' Dark variant of the Civilytics ggplot2 theme #' Dark variant of the Civilytics ggplot2 theme
#' #'
#' A convenience wrapper around [theme_civilytics()] with dark-background #' Convenience wrapper around [theme_civilytics()] with dark-background
#' defaults: navy (`#1A2E4A`) background, warm off-white (`#FAF7F2`) text, and #' defaults: navy (`#1A2E4A`) paper, warm off-white (`#FAF7F2`) ink, and
#' a lighter orange accent (`#E07840`) suitable for dark backgrounds. Pair with #' a lighter ember accent (`#E07840`). Facet strips use primary navy.
#' `make_logo_grob(variant = "dark")` to use the white logo.
#' #'
#' All parameters from [theme_civilytics()] are accepted and override the dark #' Pair with `make_logo_grob(variant = "dark")` for the white logo.
#' defaults.
#' #'
#' @inheritParams theme_civilytics #' @inheritParams theme_civilytics
#' #'
@@ -245,10 +295,6 @@ theme_civilytics <- function(
#' @examples #' @examples
#' \dontrun{ #' \dontrun{
#' library(ggplot2) #' library(ggplot2)
#' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics_dark()
#'
#' ggplot(mpg, aes(displ, hwy, colour = class)) + #' ggplot(mpg, aes(displ, hwy, colour = class)) +
#' geom_point() + #' geom_point() +
#' scale_color_civilytics() + #' scale_color_civilytics() +
@@ -263,9 +309,11 @@ theme_civilytics_dark <- function(
rel_tiny = 11 / 14, rel_tiny = 11 / 14,
rel_large = 16 / 14, rel_large = 16 / 14,
ink = unname(civilytics_colors["paper"]), ink = unname(civilytics_colors["paper"]),
paper = unname(civilytics_colors["navy_dark"]), paper = unname(civilytics_colors["navy_700"]),
accent = unname(civilytics_colors["accent_dark"]), accent = unname(civilytics_colors["ember_400"]),
strip_color = unname(civilytics_colors["navy"])) { strip_color = unname(civilytics_colors["navy_600"]),
grid = c("y", "x", "both", "none"),
paper_bg = TRUE) {
theme_civilytics( theme_civilytics(
font_size = font_size, font_size = font_size,
@@ -278,6 +326,68 @@ theme_civilytics_dark <- function(
ink = ink, ink = ink,
paper = paper, paper = paper,
accent = accent, accent = accent,
strip_color = strip_color 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 = 16 / 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)
)
}
+213
View File
@@ -0,0 +1,213 @@
---
output: github_document
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.path = "man/figures/README-",
fig.retina = 2,
dpi = 150,
out.width = "100%"
)
```
# civilytics
Brand themes, color palettes, and utility functions for
[Civilytics Consulting](https://www.civilytics.com). The package provides a
complete ggplot2 theme system drawn from the Civilytics design system ---
warm paper backgrounds, civic-navy ink, and editorial typography --- along
with 10 curated color palettes, logo composition helpers, and data-wrangling
utilities for public-sector analysis.
## Installation
Install from the Civilytics Gitea server:
```r
# install.packages("remotes")
remotes::install_gitea(
"Civilytics/civilyticsR",
gitea_url = "https://gitea.civilytics.org"
)
```
## Quick start
```{r quickstart, fig.height = 4, fig.width = 7, message = FALSE}
library(civilytics)
library(ggplot2)
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point(size = 2.5) +
scale_color_civilytics() +
labs(
title = "Fuel economy by engine displacement",
subtitle = "Highway MPG vs. engine size for 234 vehicles",
caption = "Source: EPA fuel economy data (ggplot2::mpg)",
x = "Engine displacement (litres)",
y = "Highway MPG"
) +
theme_civilytics()
```
## Color palettes
The package ships 53 named brand colors in `civilytics_colors` and 10
curated palettes in `civilytics_palettes`. Use `civilytics_palette()` to
retrieve colors by name, or pass palettes directly to the ggplot2 scales.
### Palette gallery
```{r palette-gallery, echo = FALSE, fig.height = 9, fig.width = 7}
show_palette <- function(name, colors) {
n <- length(colors)
df <- data.frame(
x = seq_len(n),
fill = factor(seq_len(n), levels = seq_len(n))
)
ggplot(df, aes(x, y = 1, fill = fill)) +
geom_tile(width = 0.9, height = 0.9, show.legend = FALSE) +
scale_fill_manual(values = colors) +
scale_x_continuous(expand = expansion(add = 0.5)) +
labs(title = name) +
theme_void() +
theme(
plot.title = element_text(
family = "Libre Franklin", face = "bold", size = 11,
hjust = 0, margin = margin(b = 2)
),
plot.margin = margin(4, 4, 4, 4)
)
}
plots <- mapply(
show_palette,
names(civilytics_palettes),
civilytics_palettes,
SIMPLIFY = FALSE
)
gridExtra::grid.arrange(grobs = plots, ncol = 1)
```
### Using palettes
```{r palette-usage, fig.height = 3.5, fig.width = 7}
# Discrete fill with the qualitative palette
ggplot(mpg, aes(class, fill = class)) +
geom_bar(show.legend = FALSE) +
scale_fill_civilytics() +
labs(title = "Vehicle counts by class", x = NULL, y = NULL) +
theme_civilytics(grid = "y")
# Continuous fill with a sequential palette
ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
geom_tile() +
scale_fill_civilytics("seq_ember", discrete = FALSE) +
labs(title = "Old Faithful eruption density") +
theme_civilytics(grid = "none")
```
## Themes
Three theme variants cover the most common output contexts. All share the
same typographic structure and accept `grid` and `paper_bg` parameters.
### Editorial (default)
The default theme uses a warm paper background with horizontal gridlines ---
an editorial, Pew-style layout.
```{r theme-editorial, fig.height = 4, fig.width = 7}
base_plot <- ggplot(mpg, aes(displ, hwy)) +
geom_point(aes(colour = factor(cyl)), size = 2) +
scale_color_civilytics() +
labs(
title = "Engine size vs. highway fuel economy",
subtitle = "Colored by number of cylinders",
caption = "Source: ggplot2::mpg",
colour = "Cylinders",
x = "Displacement (L)", y = "Highway MPG"
)
base_plot + theme_civilytics()
```
### Grid options
The `grid` parameter controls which major gridlines are drawn.
```{r theme-grids, echo = FALSE, fig.height = 6.5, fig.width = 7}
grid_opts <- c("y", "x", "both", "none")
grid_plots <- lapply(grid_opts, function(g) {
base_plot +
theme_civilytics(grid = g) +
labs(title = paste0("grid = \"", g, "\""), subtitle = NULL, caption = NULL)
})
gridExtra::grid.arrange(grobs = grid_plots, ncol = 2)
```
### Dark
Dark navy background with light text, suitable for presentations or
dashboards on dark surfaces.
```{r theme-dark, fig.height = 4, fig.width = 7}
base_plot + theme_civilytics_dark()
```
### Slide
Transparent background and larger base font (18 pt), sized for Reveal.js
slides or PowerPoint exports.
```{r theme-slide, fig.height = 4, fig.width = 7}
base_plot + theme_civilytics_slide()
```
### Facets
Facet strips use the `paper_2` tint, with the title font.
```{r theme-facets, fig.height = 5, fig.width = 7}
ggplot(mpg, aes(displ, hwy)) +
geom_point(colour = civilytics_colors["navy_600"], size = 1.5) +
facet_wrap(~class, ncol = 4) +
labs(
title = "Highway MPG by vehicle class",
x = "Displacement (L)", y = "Highway MPG"
) +
theme_civilytics(grid = "y")
```
## Logo utilities
Add the Civilytics logo to any ggplot using the pipe-friendly
`civilytics_logo()` or the lower-level `add_logo()` / `make_logo_grob()`.
```r
library(civilytics)
library(ggplot2)
(ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics()) |>
civilytics_logo()
```
## Other utilities
The package also includes helpers for public-sector data analysis:
| Function | Purpose |
|:---------|:--------|
| `pretty_count()` / `pretty_per()` | Format numbers and percentages |
| `grade_level_to_num()` | Convert grade labels (KG, 01--12) to numeric |
| `race_short_names()` | Standardize NCES race/ethnicity categories |
| `get_fips()` / `get_stabbr()` | State FIPS code lookups |
| `clopper_pearson()` / `agresti_coull_interval()` | Proportion confidence intervals |
| `match_test()` / `trunc_match()` | Fuzzy join diagnostics |
| `perturb_count()` / `random_round()` | Privacy-preserving data perturbation |
+181 -25
View File
@@ -1,25 +1,181 @@
# civilytics # civilytics
<!-- badges: start --> Brand themes, color palettes, and utility functions for [Civilytics
<!-- badges: end --> Consulting](https://www.civilytics.com). The package provides a complete
ggplot2 theme system drawn from the Civilytics design system — warm
The goal of civilytics is to ... paper backgrounds, civic-navy ink, and editorial typography — along with
10 curated color palettes, logo composition helpers, and data-wrangling
## Installation utilities for public-sector analysis.
You can install the released version of civilytics from [CRAN](https://CRAN.R-project.org) with: ## Installation
``` r Install from the Civilytics Gitea server:
install.packages("civilytics")
``` ``` r
# install.packages("remotes")
## Example remotes::install_gitea(
"Civilytics/civilyticsR",
This is a basic example which shows you how to solve a common problem: gitea_url = "https://gitea.civilytics.org"
)
``` r ```
library(civilytics)
## basic example code ## Quick start
```
``` r
library(civilytics)
library(ggplot2)
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point(size = 2.5) +
scale_color_civilytics() +
labs(
title = "Fuel economy by engine displacement",
subtitle = "Highway MPG vs. engine size for 234 vehicles",
caption = "Source: EPA fuel economy data (ggplot2::mpg)",
x = "Engine displacement (litres)",
y = "Highway MPG"
) +
theme_civilytics()
```
<img src="man/figures/README-quickstart-1.png" alt="" width="100%" />
## Color palettes
The package ships 53 named brand colors in `civilytics_colors` and 10
curated palettes in `civilytics_palettes`. Use `civilytics_palette()` to
retrieve colors by name, or pass palettes directly to the ggplot2
scales.
### Palette gallery
<img src="man/figures/README-palette-gallery-1.png" alt="" width="100%" />
### Using palettes
``` r
# Discrete fill with the qualitative palette
ggplot(mpg, aes(class, fill = class)) +
geom_bar(show.legend = FALSE) +
scale_fill_civilytics() +
labs(title = "Vehicle counts by class", x = NULL, y = NULL) +
theme_civilytics(grid = "y")
```
<img src="man/figures/README-palette-usage-1.png" alt="" width="100%" />
``` r
# Continuous fill with a sequential palette
ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
geom_tile() +
scale_fill_civilytics("seq_ember", discrete = FALSE) +
labs(title = "Old Faithful eruption density") +
theme_civilytics(grid = "none")
```
<img src="man/figures/README-palette-usage-2.png" alt="" width="100%" />
## Themes
Three theme variants cover the most common output contexts. All share
the same typographic structure and accept `grid` and `paper_bg`
parameters.
### Editorial (default)
The default theme uses a warm paper background with horizontal gridlines
— an editorial, Pew-style layout.
``` r
base_plot <- ggplot(mpg, aes(displ, hwy)) +
geom_point(aes(colour = factor(cyl)), size = 2) +
scale_color_civilytics() +
labs(
title = "Engine size vs. highway fuel economy",
subtitle = "Colored by number of cylinders",
caption = "Source: ggplot2::mpg",
colour = "Cylinders",
x = "Displacement (L)", y = "Highway MPG"
)
base_plot + theme_civilytics()
```
<img src="man/figures/README-theme-editorial-1.png" alt="" width="100%" />
### Grid options
The `grid` parameter controls which major gridlines are drawn.
<img src="man/figures/README-theme-grids-1.png" alt="" width="100%" />
### Dark
Dark navy background with light text, suitable for presentations or
dashboards on dark surfaces.
``` r
base_plot + theme_civilytics_dark()
```
<img src="man/figures/README-theme-dark-1.png" alt="" width="100%" />
### Slide
Transparent background and larger base font (18 pt), sized for Reveal.js
slides or PowerPoint exports.
``` r
base_plot + theme_civilytics_slide()
```
<img src="man/figures/README-theme-slide-1.png" alt="" width="100%" />
### Facets
Facet strips use the `paper_2` tint, with the title font.
``` r
ggplot(mpg, aes(displ, hwy)) +
geom_point(colour = civilytics_colors["navy_600"], size = 1.5) +
facet_wrap(~class, ncol = 4) +
labs(
title = "Highway MPG by vehicle class",
x = "Displacement (L)", y = "Highway MPG"
) +
theme_civilytics(grid = "y")
```
<img src="man/figures/README-theme-facets-1.png" alt="" width="100%" />
## Logo utilities
Add the Civilytics logo to any ggplot using the pipe-friendly
`civilytics_logo()` or the lower-level `add_logo()` /
`make_logo_grob()`.
``` r
library(civilytics)
library(ggplot2)
(ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics()) |>
civilytics_logo()
```
## Other utilities
The package also includes helpers for public-sector data analysis:
| Function | Purpose |
|:---|:---|
| `pretty_count()` / `pretty_per()` | Format numbers and percentages |
| `grade_level_to_num()` | Convert grade labels (KG, 01–12) to numeric |
| `race_short_names()` | Standardize NCES race/ethnicity categories |
| `get_fips()` / `get_stabbr()` | State FIPS code lookups |
| `clopper_pearson()` / `agresti_coull_interval()` | Proportion confidence intervals |
| `match_test()` / `trunc_match()` | Fuzzy join diagnostics |
| `perturb_count()` / `random_round()` | Privacy-preserving data perturbation |
+24 -2
View File
@@ -4,10 +4,32 @@
\name{civilytics-package} \name{civilytics-package}
\alias{civilytics} \alias{civilytics}
\alias{civilytics-package} \alias{civilytics-package}
\title{civilytics: Utilities Functions for Civilytics} \title{civilytics: Brand Themes, Color Palettes, and Utility Functions for Civilytics}
\description{ \description{
House R functions for Civilytics Consulting LLC. This package implements a variety of useful functions for creating and branding analyses produced by Civilytics Consulting LLC. Provides a complete ggplot2 brand theme system for Civilytics Consulting,
including editorial light, dark, and slide-optimized themes; 10 curated
color palettes; logo composition; and data-wrangling helpers for
public-sector analysis.
} }
\section{Themes}{
\itemize{
\item [theme_civilytics()] -- editorial theme with warm paper background
\item [theme_civilytics_dark()] -- navy background variant
\item [theme_civilytics_slide()] -- transparent background, larger text
}
}
\section{Color palettes}{
\itemize{
\item [civilytics_colors] -- 53 named brand colors
\item [civilytics_palettes] -- 10 visualization palettes
\item [civilytics_palette()] -- extract colors by palette name
\item [scale_color_civilytics()] / [scale_fill_civilytics()] -- ggplot2 scales
}
}
\seealso{ \seealso{
Useful links: Useful links:
\itemize{ \itemize{
+5 -3
View File
@@ -12,10 +12,12 @@ civilytics_colors
} }
\description{ \description{
A named character vector of all Civilytics brand colors, matching the CSS A named character vector of all Civilytics brand colors, matching the CSS
custom properties defined on www.civilytics.com (`--cv-*` variables). custom properties in the Civilytics design system (`--cv-*` variables).
Includes full ramps for navy, ember, and violet, plus supporting hues
(teal, plum, moss, brass) and semantic status colors.
} }
\examples{ \examples{
civilytics_colors["accent"] civilytics_colors["ink"]
civilytics_colors[c("ink", "paper")] civilytics_colors[c("navy_600", "ember_600", "teal_600")]
} }
\keyword{datasets} \keyword{datasets}
+9 -14
View File
@@ -2,28 +2,23 @@
% Please edit documentation in R/colors.R % Please edit documentation in R/colors.R
\name{civilytics_pal} \name{civilytics_pal}
\alias{civilytics_pal} \alias{civilytics_pal}
\title{Civilytics brand color palette} \title{Civilytics palette function (closure)}
\usage{ \usage{
civilytics_pal(name = "main", n = NULL) civilytics_pal(name = "qual", reverse = FALSE)
} }
\arguments{ \arguments{
\item{name}{Character. Palette name: `"main"`, `"sequential"`, or \item{name}{Character. Palette name. See `names(civilytics_palettes)`.}
`"diverging"`. Defaults to `"main"`.}
\item{n}{Integer or `NULL`. Number of colors to return. If `NULL`, returns \item{reverse}{Logical. Reverse the palette order. Default `FALSE`.}
all colors in the palette. If `n` exceeds the number of defined stops,
colors are interpolated via [grDevices::colorRampPalette()].}
} }
\value{ \value{
A character vector of hex color codes. A function that takes integer `n` and returns `n` hex color codes.
} }
\description{ \description{
Returns a character vector of hex codes from a named Civilytics palette. Returns a closure `function(n)` suitable for passing to
Available palettes: `"main"` (qualitative, up to 6), `"sequential"` (light [ggplot2::discrete_scale()] or similar scale constructors.
to dark navy), `"diverging"` (orange–neutral–navy).
} }
\examples{ \examples{
civilytics_pal() # all 6 qualitative colors pal_fn <- civilytics_pal("qual")
civilytics_pal("sequential", n = 3) pal_fn(4)
civilytics_pal("diverging", n = 5)
} }
+30
View File
@@ -0,0 +1,30 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/colors.R
\name{civilytics_palette}
\alias{civilytics_palette}
\title{Get colors from a Civilytics palette}
\usage{
civilytics_palette(name = "qual", n = NULL, reverse = FALSE)
}
\arguments{
\item{name}{Character. Palette name. See `names(civilytics_palettes)`.}
\item{n}{Integer or `NULL`. Number of colors to return. If `NULL`, returns
all defined stops.}
\item{reverse}{Logical. Reverse the palette order. Default `FALSE`.}
}
\value{
A character vector of hex color codes.
}
\description{
Returns a character vector of hex codes from a named Civilytics palette.
For qualitative palettes, colors beyond the palette length recycle with a
warning. For sequential and diverging palettes, colors are interpolated
via [grDevices::colorRampPalette()].
}
\examples{
civilytics_palette() # all 7 qualitative colors
civilytics_palette("seq_navy", n = 5) # 5-stop navy ramp
civilytics_palette("div_navy_ember", n = 11, reverse = TRUE)
}
+50
View File
@@ -0,0 +1,50 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/colors.R
\docType{data}
\name{civilytics_palettes}
\alias{civilytics_palettes}
\title{Civilytics visualization palettes}
\format{
A named list of character vectors of hex color codes.
}
\usage{
civilytics_palettes
}
\description{
Named list of curated color palettes for data visualization. Qualitative
palettes use distinct hues for categorical data; sequential palettes ramp
through a single hue for ordered data; diverging palettes fan out from a
neutral midpoint.
}
\section{Qualitative (categorical data)}{
\describe{
\item{`qual`}{7 distinct hues: navy, ember, plum, violet, red, teal, ink-3}
\item{`qual_warm`}{Warm-leaning: ember, red, magenta, plum, violet}
\item{`qual_cool`}{Cool-leaning: navy, violet, plum, teal}
}
}
\section{Sequential (ordered/continuous data)}{
\describe{
\item{`seq_ember`}{Light to dark ember (9 stops)}
\item{`seq_navy`}{Light to dark navy (9 stops)}
\item{`seq_violet`}{Light to dark violet (9 stops)}
\item{`seq_paper_ink`}{Paper through ember/plum to ink (8 stops, good for heatmaps)}
}
}
\section{Diverging (data with a meaningful midpoint)}{
\describe{
\item{`div_navy_ember`}{Navy <-> paper <-> ember (9 stops)}
\item{`div_violet_ember`}{Violet <-> paper <-> ember (9 stops)}
}
}
\examples{
names(civilytics_palettes)
civilytics_palettes[["qual"]]
}
\keyword{datasets}
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

+9 -11
View File
@@ -4,25 +4,23 @@
\alias{scale_color_civilytics} \alias{scale_color_civilytics}
\title{Civilytics color scale for ggplot2} \title{Civilytics color scale for ggplot2}
\usage{ \usage{
scale_color_civilytics(palette = "main", discrete = TRUE, ...) scale_color_civilytics(palette = "qual", discrete = TRUE, reverse = FALSE, ...)
} }
\arguments{ \arguments{
\item{palette}{Character. Palette name passed to [civilytics_pal()]. \item{palette}{Character. Palette name. Defaults to `"qual"`.}
Defaults to `"main"`.}
\item{discrete}{Logical. `TRUE` (default) for a discrete scale; `FALSE` for \item{discrete}{Logical. `TRUE` (default) for categorical data; `FALSE`
a continuous gradient via [ggplot2::scale_color_gradientn()].} for a continuous gradient via [ggplot2::scale_color_gradientn()].}
\item{...}{Additional arguments passed to the underlying ggplot2 scale \item{reverse}{Logical. Reverse the palette order. Default `FALSE`.}
function.}
\item{...}{Additional arguments passed to the ggplot2 scale function.}
} }
\value{ \value{
A ggplot2 scale object. A ggplot2 scale object.
} }
\description{ \description{
Applies a Civilytics brand palette to the `colour` aesthetic. Use Applies a Civilytics brand palette to the `colour` aesthetic.
`discrete = TRUE` for categorical variables and `discrete = FALSE` for
continuous gradients.
} }
\examples{ \examples{
library(ggplot2) library(ggplot2)
@@ -32,5 +30,5 @@ ggplot(mpg, aes(displ, hwy, colour = class)) +
ggplot(mpg, aes(displ, hwy, colour = cty)) + ggplot(mpg, aes(displ, hwy, colour = cty)) +
geom_point() + geom_point() +
scale_color_civilytics("sequential", discrete = FALSE) scale_color_civilytics("seq_navy", discrete = FALSE)
} }
+9 -11
View File
@@ -4,25 +4,23 @@
\alias{scale_fill_civilytics} \alias{scale_fill_civilytics}
\title{Civilytics fill scale for ggplot2} \title{Civilytics fill scale for ggplot2}
\usage{ \usage{
scale_fill_civilytics(palette = "main", discrete = TRUE, ...) scale_fill_civilytics(palette = "qual", discrete = TRUE, reverse = FALSE, ...)
} }
\arguments{ \arguments{
\item{palette}{Character. Palette name passed to [civilytics_pal()]. \item{palette}{Character. Palette name. Defaults to `"qual"`.}
Defaults to `"main"`.}
\item{discrete}{Logical. `TRUE` (default) for a discrete scale; `FALSE` for \item{discrete}{Logical. `TRUE` (default) for categorical data; `FALSE`
a continuous gradient via [ggplot2::scale_fill_gradientn()].} for a continuous gradient via [ggplot2::scale_fill_gradientn()].}
\item{...}{Additional arguments passed to the underlying ggplot2 scale \item{reverse}{Logical. Reverse the palette order. Default `FALSE`.}
function.}
\item{...}{Additional arguments passed to the ggplot2 scale function.}
} }
\value{ \value{
A ggplot2 scale object. A ggplot2 scale object.
} }
\description{ \description{
Applies a Civilytics brand palette to the `fill` aesthetic. Use Applies a Civilytics brand palette to the `fill` aesthetic.
`discrete = TRUE` for categorical variables and `discrete = FALSE` for
continuous gradients.
} }
\examples{ \examples{
library(ggplot2) library(ggplot2)
@@ -32,5 +30,5 @@ ggplot(mpg, aes(class, fill = class)) +
ggplot(faithfuld, aes(waiting, eruptions, fill = density)) + ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
geom_tile() + geom_tile() +
scale_fill_civilytics("sequential", discrete = FALSE) scale_fill_civilytics("seq_ember", discrete = FALSE)
} }
+27 -4
View File
@@ -14,8 +14,10 @@ theme_civilytics(
rel_large = 16/14, rel_large = 16/14,
ink = unname(civilytics_colors["ink"]), ink = unname(civilytics_colors["ink"]),
paper = unname(civilytics_colors["paper"]), paper = unname(civilytics_colors["paper"]),
accent = unname(civilytics_colors["accent"]), 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"),
paper_bg = TRUE
) )
} }
\arguments{ \arguments{
@@ -45,10 +47,18 @@ Default `11/14`.}
[civilytics_colors]`["paper"]` (`#FAF7F2`).} [civilytics_colors]`["paper"]` (`#FAF7F2`).}
\item{accent}{Character. Hex code for accent/highlight color. Defaults to \item{accent}{Character. Hex code for accent/highlight color. Defaults to
[civilytics_colors]`["accent"]` (`#C25311`).} [civilytics_colors]`["ember_600"]` (`#C25311`).}
\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{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).}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
@@ -59,6 +69,11 @@ Civilytics brand color palette and typography. Requires ggplot2 >= 4.0.0
for the `ink`, `paper`, and `accent` base-theme parameters. for the `ink`, `paper`, and `accent` base-theme parameters.
} }
\details{ \details{
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 Brand fonts (Inter for UI text, Libre Franklin for titles) are loaded
automatically via [showtext] when the package is attached. Call automatically via [showtext] when the package is attached. Call
[civilytics_load_fonts()] to reload them if needed. [civilytics_load_fonts()] to reload them if needed.
@@ -66,13 +81,21 @@ automatically via [showtext] when the package is attached. Call
\examples{ \examples{
\dontrun{ \dontrun{
library(ggplot2) library(ggplot2)
# Default editorial theme
ggplot(mpg, aes(displ, hwy)) + ggplot(mpg, aes(displ, hwy)) +
geom_point() + geom_point() +
theme_civilytics() theme_civilytics()
# With both gridlines and brand colors
ggplot(mpg, aes(displ, hwy, colour = class)) + ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() + geom_point() +
scale_color_civilytics() + scale_color_civilytics() +
theme_civilytics() theme_civilytics(grid = "both")
# Transparent background for embedding
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics(paper_bg = FALSE)
} }
} }
+18 -14
View File
@@ -13,9 +13,11 @@ theme_civilytics_dark(
rel_tiny = 11/14, rel_tiny = 11/14,
rel_large = 16/14, rel_large = 16/14,
ink = unname(civilytics_colors["paper"]), ink = unname(civilytics_colors["paper"]),
paper = unname(civilytics_colors["navy_dark"]), paper = unname(civilytics_colors["navy_700"]),
accent = unname(civilytics_colors["accent_dark"]), accent = unname(civilytics_colors["ember_400"]),
strip_color = unname(civilytics_colors["navy"]) strip_color = unname(civilytics_colors["navy_600"]),
grid = c("y", "x", "both", "none"),
paper_bg = TRUE
) )
} }
\arguments{ \arguments{
@@ -45,31 +47,33 @@ Default `11/14`.}
[civilytics_colors]`["paper"]` (`#FAF7F2`).} [civilytics_colors]`["paper"]` (`#FAF7F2`).}
\item{accent}{Character. Hex code for accent/highlight color. Defaults to \item{accent}{Character. Hex code for accent/highlight color. Defaults to
[civilytics_colors]`["accent"]` (`#C25311`).} [civilytics_colors]`["ember_600"]` (`#C25311`).}
\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{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).}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
} }
\description{ \description{
A convenience wrapper around [theme_civilytics()] with dark-background Convenience wrapper around [theme_civilytics()] with dark-background
defaults: navy (`#1A2E4A`) background, warm off-white (`#FAF7F2`) text, and defaults: navy (`#1A2E4A`) paper, warm off-white (`#FAF7F2`) ink, and
a lighter orange accent (`#E07840`) suitable for dark backgrounds. Pair with a lighter ember accent (`#E07840`). Facet strips use primary navy.
`make_logo_grob(variant = "dark")` to use the white logo.
} }
\details{ \details{
All parameters from [theme_civilytics()] are accepted and override the dark Pair with `make_logo_grob(variant = "dark")` for the white logo.
defaults.
} }
\examples{ \examples{
\dontrun{ \dontrun{
library(ggplot2) library(ggplot2)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics_dark()
ggplot(mpg, aes(displ, hwy, colour = class)) + ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() + geom_point() +
scale_color_civilytics() + scale_color_civilytics() +
+79
View File
@@ -0,0 +1,79 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/theme.R
\name{theme_civilytics_slide}
\alias{theme_civilytics_slide}
\title{Slide-friendly Civilytics ggplot2 theme}
\usage{
theme_civilytics_slide(
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 = 16/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
)
}
\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]`["ember_600"]` (`#C25311`).}
\item{strip_color}{Character. Hex code for facet strip background. Defaults
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).}
}
\value{
A complete ggplot2 [ggplot2::theme()] object.
}
\description{
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.
}
\examples{
\dontrun{
library(ggplot2)
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() +
scale_color_civilytics() +
theme_civilytics_slide()
}
}
+122 -27
View File
@@ -9,46 +9,67 @@ test_that("civilytics_colors is a named character vector of hex codes", {
}) })
test_that("civilytics_colors contains expected brand keys", { test_that("civilytics_colors contains expected brand keys", {
expected <- c("paper", "ink", "accent", "navy", "navy_dark", expected <- c("paper", "ink", "navy_600", "ember_600", "teal_600",
"accent_dark", "paper_2", "rule") "plum_600", "moss_600", "paper_2", "rule")
expect_true(all(expected %in% names(civilytics_colors))) expect_true(all(expected %in% names(civilytics_colors)))
}) })
test_that("civilytics_colors values match brand specification", { test_that("civilytics_colors values match brand specification", {
expect_equal(unname(civilytics_colors["paper"]), "#FAF7F2") expect_equal(unname(civilytics_colors["paper"]), "#FAF7F2")
expect_equal(unname(civilytics_colors["ink"]), "#0E1A2B") expect_equal(unname(civilytics_colors["ink"]), "#0E1A2B")
expect_equal(unname(civilytics_colors["accent"]), "#C25311") expect_equal(unname(civilytics_colors["ember_600"]), "#C25311")
expect_equal(unname(civilytics_colors["navy_dark"]), "#1A2E4A") expect_equal(unname(civilytics_colors["navy_700"]), "#1A2E4A")
expect_equal(unname(civilytics_colors["accent_dark"]),"#E07840") expect_equal(unname(civilytics_colors["ember_400"]), "#EA8A49")
}) })
# --- civilytics_pal ---------------------------------------------------------- # --- civilytics_palettes / civilytics_palette --------------------------------
test_that("civilytics_pal returns character vector of hex codes", { test_that("civilytics_palettes contains all expected palettes", {
result <- civilytics_pal() expected <- c("qual", "qual_warm", "qual_cool",
"seq_ember", "seq_navy", "seq_violet", "seq_paper_ink",
"div_navy_ember", "div_violet_ember")
expect_true(all(expected %in% names(civilytics_palettes)))
})
test_that("civilytics_palette returns character vector of hex codes", {
result <- civilytics_palette()
expect_type(result, "character") expect_type(result, "character")
expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", result))) expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", result)))
}) })
test_that("civilytics_pal respects n argument", { test_that("civilytics_palette respects n argument", {
expect_length(civilytics_pal("main", n = 3), 3) expect_length(civilytics_palette("qual", n = 3), 3)
expect_length(civilytics_pal("sequential", n = 5), 5) expect_length(civilytics_palette("seq_navy", n = 5), 5)
}) })
test_that("civilytics_pal interpolates when n exceeds palette size", { test_that("civilytics_palette interpolates sequential palettes", {
result <- civilytics_pal("main", n = 10) result <- civilytics_palette("seq_ember", n = 20)
expect_length(result, 10) expect_length(result, 20)
expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", result))) expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", result)))
}) })
test_that("civilytics_pal errors on unknown palette name", { test_that("civilytics_palette warns when recycling qualitative", {
expect_error(civilytics_pal("nope"), "'nope' is not a valid Civilytics palette") expect_warning(civilytics_palette("qual", n = 10), "recycling")
}) })
test_that("civilytics_pal supports all three named palettes", { test_that("civilytics_palette reverse works", {
expect_no_error(civilytics_pal("main")) fwd <- civilytics_palette("seq_navy")
expect_no_error(civilytics_pal("sequential")) rev <- civilytics_palette("seq_navy", reverse = TRUE)
expect_no_error(civilytics_pal("diverging")) expect_equal(fwd, rev(rev))
})
test_that("civilytics_palette errors on unknown palette name", {
expect_error(civilytics_palette("nope"), "'nope' is not a valid palette")
})
# --- civilytics_pal (closure) ------------------------------------------------
test_that("civilytics_pal returns a function", {
f <- civilytics_pal()
expect_type(f, "closure")
result <- f(4)
expect_length(result, 4)
expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", result)))
}) })
# --- scale_color/fill_civilytics --------------------------------------------- # --- scale_color/fill_civilytics ---------------------------------------------
@@ -59,7 +80,7 @@ test_that("scale_color_civilytics returns a Scale object (discrete)", {
}) })
test_that("scale_color_civilytics returns a Scale object (continuous)", { test_that("scale_color_civilytics returns a Scale object (continuous)", {
sc <- scale_color_civilytics("sequential", discrete = FALSE) sc <- scale_color_civilytics("seq_navy", discrete = FALSE)
expect_s3_class(sc, "Scale") expect_s3_class(sc, "Scale")
}) })
@@ -69,7 +90,12 @@ test_that("scale_fill_civilytics returns a Scale object (discrete)", {
}) })
test_that("scale_fill_civilytics returns a Scale object (continuous)", { test_that("scale_fill_civilytics returns a Scale object (continuous)", {
sc <- scale_fill_civilytics("sequential", discrete = FALSE) sc <- scale_fill_civilytics("seq_ember", discrete = FALSE)
expect_s3_class(sc, "Scale")
})
test_that("scale_color_civilytics reverse parameter works", {
sc <- scale_color_civilytics("qual", reverse = TRUE)
expect_s3_class(sc, "Scale") expect_s3_class(sc, "Scale")
}) })
@@ -128,6 +154,42 @@ test_that("theme_civilytics font_size parameter scales text elements", {
expect_gt(th_big$text$size, th_sml$text$size) expect_gt(th_big$text$size, th_sml$text$size)
}) })
test_that("theme_civilytics grid parameter controls gridlines", {
th_y <- theme_civilytics(grid = "y")
th_x <- theme_civilytics(grid = "x")
th_both <- theme_civilytics(grid = "both")
th_none <- theme_civilytics(grid = "none")
# grid="y" shows y gridlines, blanks x
expect_s3_class(th_y$panel.grid.major.y, "element_line")
expect_s3_class(th_y$panel.grid.major.x, "element_blank")
# grid="x" shows x gridlines, blanks y
expect_s3_class(th_x$panel.grid.major.x, "element_line")
expect_s3_class(th_x$panel.grid.major.y, "element_blank")
# grid="both" shows both
expect_s3_class(th_both$panel.grid.major.x, "element_line")
expect_s3_class(th_both$panel.grid.major.y, "element_line")
# grid="none" blanks both
expect_s3_class(th_none$panel.grid.major.x, "element_blank")
expect_s3_class(th_none$panel.grid.major.y, "element_blank")
})
test_that("theme_civilytics paper_bg=FALSE gives transparent background", {
th <- theme_civilytics(paper_bg = FALSE)
expect_true(is.na(th$plot.background$fill))
expect_true(is.na(th$panel.background$fill))
})
test_that("theme_civilytics has plot-aligned title and caption", {
th <- theme_civilytics()
expect_equal(th$plot.title.position, "plot")
expect_equal(th$plot.caption.position, "plot")
})
# --- theme_civilytics_dark --------------------------------------------------- # --- theme_civilytics_dark ---------------------------------------------------
test_that("theme_civilytics_dark returns a complete ggplot2 theme", { test_that("theme_civilytics_dark returns a complete ggplot2 theme", {
@@ -148,14 +210,47 @@ test_that("theme_civilytics_dark uses paper color as ink (light text on dark bg)
expect_equal(th$text$colour, unname(civilytics_colors["paper"])) expect_equal(th$text$colour, unname(civilytics_colors["paper"]))
}) })
test_that("theme_civilytics_dark uses navy_dark as background", { test_that("theme_civilytics_dark uses navy_700 as background", {
th <- theme_civilytics_dark() th <- theme_civilytics_dark()
expect_equal(th$plot.background$fill, unname(civilytics_colors["navy_dark"])) expect_equal(th$plot.background$fill, unname(civilytics_colors["navy_700"]))
}) })
test_that("theme_civilytics_dark uses navy for strip background", { test_that("theme_civilytics_dark uses navy_600 for strip background", {
th <- theme_civilytics_dark() th <- theme_civilytics_dark()
expect_equal(th$strip.background$fill, unname(civilytics_colors["navy"])) expect_equal(th$strip.background$fill, unname(civilytics_colors["navy_600"]))
})
test_that("theme_civilytics_dark inherits grid parameter", {
th <- theme_civilytics_dark(grid = "both")
expect_s3_class(th$panel.grid.major.x, "element_line")
expect_s3_class(th$panel.grid.major.y, "element_line")
})
# --- theme_civilytics_slide --------------------------------------------------
test_that("theme_civilytics_slide returns a complete ggplot2 theme", {
th <- theme_civilytics_slide()
expect_s3_class(th, "theme")
expect_true(attr(th, "complete"))
})
test_that("theme_civilytics_slide has transparent background by default", {
th <- theme_civilytics_slide()
expect_true(is.na(th$plot.background$fill))
})
test_that("theme_civilytics_slide uses larger base font", {
th_slide <- theme_civilytics_slide()
th_base <- theme_civilytics()
expect_gt(th_slide$text$size, th_base$text$size)
})
test_that("theme_civilytics_slide applies to a ggplot without error", {
p <- ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() +
scale_color_civilytics() +
theme_civilytics_slide()
expect_no_error(ggplot_build(p))
}) })
# --- make_logo_grob ---------------------------------------------------------- # --- make_logo_grob ----------------------------------------------------------