diff --git a/NAMESPACE b/NAMESPACE index ba6b532..79ca718 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -44,7 +44,10 @@ export(simpleCap) export(star_subs) export(theme_civilytics) export(theme_civilytics_dark) +export(theme_civilytics_dark_map) +export(theme_civilytics_map) export(theme_civilytics_slide) +export(theme_civilytics_slide_map) export(trim_max) export(waldInterval) export(z_gap_test) diff --git a/R/theme.R b/R/theme.R index 0b3dbeb..503e5ac 100644 --- a/R/theme.R +++ b/R/theme.R @@ -355,7 +355,27 @@ theme_civilytics_dark <- function( strip_color = strip_color, grid = grid, paper_bg = paper_bg - ) + ) + + # The base theme hardcodes ink_2/ink_3 for subtitle/caption, which are + # dark colors meant for light backgrounds. Override with lighter values + # so text remains readable on the navy background. + ggplot2::theme( + plot.subtitle = ggplot2::element_text( + color = unname(civilytics_colors["navy_200"]) + ), + plot.caption = ggplot2::element_text( + color = unname(civilytics_colors["navy_300"]) + ), + axis.text = ggplot2::element_text( + color = unname(civilytics_colors["navy_200"]) + ), + axis.title.x = ggplot2::element_text( + color = unname(civilytics_colors["navy_300"]) + ), + axis.title.y = ggplot2::element_text( + color = unname(civilytics_colors["navy_300"]) + ) + ) } @@ -417,3 +437,186 @@ theme_civilytics_slide <- function( plot.margin = ggplot2::margin(24, 24, 24, 24) ) } + + +# -- Map themes ---------------------------------------------------------------- + +#' Shared map-theme overrides +#' +#' Strips away axes, ticks, gridlines, and axis titles/labels — the elements +#' that are meaningless on a choropleth or spatial plot. +#' +#' @return A partial ggplot2 [ggplot2::theme()] object. +#' @keywords internal +.map_theme_extras <- function() { + ggplot2::theme( + axis.line = ggplot2::element_blank(), + axis.line.x = ggplot2::element_blank(), + axis.line.y = ggplot2::element_blank(), + axis.text = ggplot2::element_blank(), + axis.text.x = ggplot2::element_blank(), + axis.text.y = ggplot2::element_blank(), + axis.ticks = ggplot2::element_blank(), + axis.ticks.length = ggplot2::unit(0, "pt"), + axis.title.x = ggplot2::element_blank(), + axis.title.y = ggplot2::element_blank(), + panel.grid.major.x = ggplot2::element_blank(), + panel.grid.major.y = ggplot2::element_blank(), + panel.grid.minor = ggplot2::element_blank() + ) +} + + +#' Map-friendly Civilytics ggplot2 theme +#' +#' Variant of [theme_civilytics()] for choropleths and spatial plots. +#' Suppresses axes, axis labels, ticks, and gridlines while keeping the +#' Civilytics brand typography, colors, and plot-level elements (title, +#' subtitle, caption, legend). +#' +#' @inheritParams theme_civilytics +#' +#' @return A complete ggplot2 [ggplot2::theme()] object. +#' @export +#' +#' @examples +#' \dontrun{ +#' library(ggplot2) +#' # With sf data: +#' ggplot(map_data) + +#' geom_sf(aes(fill = value)) + +#' scale_fill_civilytics_c("seq_navy") + +#' theme_civilytics_map() +#' } +theme_civilytics_map <- function( + font_size = 14, + font_family = CV_FONT_SANS, + title_family = CV_FONT_DISPLAY, + line_size = 0.5, + rel_small = 12 / 14, + rel_tiny = 11 / 14, + rel_large = 20 / 14, + ink = unname(civilytics_colors["ink"]), + paper = unname(civilytics_colors["paper"]), + accent = unname(civilytics_colors["ember_600"]), + strip_color = unname(civilytics_colors["paper_2"]), + paper_bg = TRUE) { + + 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 = "none", + paper_bg = paper_bg + ) + .map_theme_extras() +} + + +#' Dark map-friendly Civilytics ggplot2 theme +#' +#' Variant of [theme_civilytics_dark()] for choropleths and spatial plots. +#' Suppresses axes, axis labels, ticks, and gridlines on a dark navy +#' background. Pair with `make_logo_grob(variant = "dark")`. +#' +#' @inheritParams theme_civilytics_dark +#' +#' @return A complete ggplot2 [ggplot2::theme()] object. +#' @export +#' +#' @examples +#' \dontrun{ +#' library(ggplot2) +#' ggplot(map_data) + +#' geom_sf(aes(fill = value)) + +#' scale_fill_civilytics_c("seq_ember") + +#' theme_civilytics_dark_map() +#' } +theme_civilytics_dark_map <- function( + font_size = 14, + font_family = CV_FONT_SANS, + title_family = CV_FONT_DISPLAY, + line_size = 0.5, + rel_small = 12 / 14, + rel_tiny = 11 / 14, + rel_large = 20 / 14, + ink = unname(civilytics_colors["paper"]), + paper = unname(civilytics_colors["navy_700"]), + accent = unname(civilytics_colors["ember_400"]), + strip_color = unname(civilytics_colors["navy_600"]), + paper_bg = TRUE) { + + theme_civilytics_dark( + 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 = "none", + paper_bg = paper_bg + ) + .map_theme_extras() +} + + +#' Slide-friendly map Civilytics ggplot2 theme +#' +#' Variant of [theme_civilytics_slide()] for choropleths and spatial plots +#' on slides. Combines the larger base font and transparent background of +#' the slide theme with suppressed axes, ticks, and gridlines. +#' +#' @inheritParams theme_civilytics_slide +#' +#' @return A complete ggplot2 [ggplot2::theme()] object. +#' @export +#' +#' @examples +#' \dontrun{ +#' library(ggplot2) +#' ggplot(map_data) + +#' geom_sf(aes(fill = value)) + +#' scale_fill_civilytics_c("seq_navy") + +#' theme_civilytics_slide_map() +#' } +theme_civilytics_slide_map <- 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 = 20 / 14, + ink = unname(civilytics_colors["ink"]), + paper = unname(civilytics_colors["paper"]), + accent = unname(civilytics_colors["ember_600"]), + strip_color = unname(civilytics_colors["paper_2"]), + paper_bg = FALSE) { + + theme_civilytics_slide( + 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 = "none", + paper_bg = paper_bg + ) + .map_theme_extras() +} diff --git a/man/dot-map_theme_extras.Rd b/man/dot-map_theme_extras.Rd new file mode 100644 index 0000000..47eed42 --- /dev/null +++ b/man/dot-map_theme_extras.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/theme.R +\name{.map_theme_extras} +\alias{.map_theme_extras} +\title{Shared map-theme overrides} +\usage{ +.map_theme_extras() +} +\value{ +A partial ggplot2 [ggplot2::theme()] object. +} +\description{ +Strips away axes, ticks, gridlines, and axis titles/labels — the elements +that are meaningless on a choropleth or spatial plot. +} +\keyword{internal} diff --git a/man/theme_civilytics_dark_map.Rd b/man/theme_civilytics_dark_map.Rd new file mode 100644 index 0000000..4d5999b --- /dev/null +++ b/man/theme_civilytics_dark_map.Rd @@ -0,0 +1,76 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/theme.R +\name{theme_civilytics_dark_map} +\alias{theme_civilytics_dark_map} +\title{Dark map-friendly Civilytics ggplot2 theme} +\usage{ +theme_civilytics_dark_map( + font_size = 14, + font_family = CV_FONT_SANS, + title_family = CV_FONT_DISPLAY, + line_size = 0.5, + rel_small = 12/14, + rel_tiny = 11/14, + rel_large = 20/14, + ink = unname(civilytics_colors["paper"]), + paper = unname(civilytics_colors["navy_700"]), + accent = unname(civilytics_colors["ember_400"]), + strip_color = unname(civilytics_colors["navy_600"]), + paper_bg = TRUE +) +} +\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 `20/14` (~1.43x), matching the Civilytics editorial +design system.} + +\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{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_dark()] for choropleths and spatial plots. +Suppresses axes, axis labels, ticks, and gridlines on a dark navy +background. Pair with `make_logo_grob(variant = "dark")`. +} +\examples{ +\dontrun{ +library(ggplot2) +ggplot(map_data) + + geom_sf(aes(fill = value)) + + scale_fill_civilytics_c("seq_ember") + + theme_civilytics_dark_map() +} +} diff --git a/man/theme_civilytics_map.Rd b/man/theme_civilytics_map.Rd new file mode 100644 index 0000000..83d9c01 --- /dev/null +++ b/man/theme_civilytics_map.Rd @@ -0,0 +1,78 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/theme.R +\name{theme_civilytics_map} +\alias{theme_civilytics_map} +\title{Map-friendly Civilytics ggplot2 theme} +\usage{ +theme_civilytics_map( + font_size = 14, + font_family = CV_FONT_SANS, + title_family = CV_FONT_DISPLAY, + line_size = 0.5, + rel_small = 12/14, + rel_tiny = 11/14, + rel_large = 20/14, + ink = unname(civilytics_colors["ink"]), + paper = unname(civilytics_colors["paper"]), + accent = unname(civilytics_colors["ember_600"]), + strip_color = unname(civilytics_colors["paper_2"]), + paper_bg = TRUE +) +} +\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 `20/14` (~1.43x), matching the Civilytics editorial +design system.} + +\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{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()] for choropleths and spatial plots. +Suppresses axes, axis labels, ticks, and gridlines while keeping the +Civilytics brand typography, colors, and plot-level elements (title, +subtitle, caption, legend). +} +\examples{ +\dontrun{ +library(ggplot2) +# With sf data: +ggplot(map_data) + + geom_sf(aes(fill = value)) + + scale_fill_civilytics_c("seq_navy") + + theme_civilytics_map() +} +} diff --git a/man/theme_civilytics_slide_map.Rd b/man/theme_civilytics_slide_map.Rd new file mode 100644 index 0000000..9d8bc45 --- /dev/null +++ b/man/theme_civilytics_slide_map.Rd @@ -0,0 +1,76 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/theme.R +\name{theme_civilytics_slide_map} +\alias{theme_civilytics_slide_map} +\title{Slide-friendly map Civilytics ggplot2 theme} +\usage{ +theme_civilytics_slide_map( + 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 = 20/14, + ink = unname(civilytics_colors["ink"]), + paper = unname(civilytics_colors["paper"]), + accent = unname(civilytics_colors["ember_600"]), + strip_color = unname(civilytics_colors["paper_2"]), + 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 `20/14` (~1.43x), matching the Civilytics editorial +design system.} + +\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{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_slide()] for choropleths and spatial plots +on slides. Combines the larger base font and transparent background of +the slide theme with suppressed axes, ticks, and gridlines. +} +\examples{ +\dontrun{ +library(ggplot2) +ggplot(map_data) + + geom_sf(aes(fill = value)) + + scale_fill_civilytics_c("seq_navy") + + theme_civilytics_slide_map() +} +} diff --git a/tests/testthat/test_theme.R b/tests/testthat/test_theme.R index 683e5b3..720f8a7 100644 --- a/tests/testthat/test_theme.R +++ b/tests/testthat/test_theme.R @@ -226,6 +226,21 @@ test_that("theme_civilytics_dark inherits grid parameter", { expect_s3_class(th$panel.grid.major.y, "element_line") }) +test_that("theme_civilytics_dark uses readable subtitle and caption colors", { + th <- theme_civilytics_dark() + # Subtitle should use navy_200, not the dark ink_2 + expect_equal(th$plot.subtitle$colour, unname(civilytics_colors["navy_200"])) + # Caption should use navy_300, not the dark ink_3 + expect_equal(th$plot.caption$colour, unname(civilytics_colors["navy_300"])) +}) + +test_that("theme_civilytics_dark uses readable axis text colors", { + th <- theme_civilytics_dark() + expect_equal(th$axis.text$colour, unname(civilytics_colors["navy_200"])) + expect_equal(th$axis.title.x$colour, unname(civilytics_colors["navy_300"])) + expect_equal(th$axis.title.y$colour, unname(civilytics_colors["navy_300"])) +}) + # --- theme_civilytics_slide -------------------------------------------------- test_that("theme_civilytics_slide returns a complete ggplot2 theme", { @@ -253,6 +268,90 @@ test_that("theme_civilytics_slide applies to a ggplot without error", { expect_no_error(ggplot_build(p)) }) +# --- theme_civilytics_map ---------------------------------------------------- + +test_that("theme_civilytics_map returns a complete theme", { + th <- theme_civilytics_map() + expect_s3_class(th, "theme") + expect_true(attr(th, "complete")) +}) + +test_that("theme_civilytics_map suppresses axes and gridlines", { + th <- theme_civilytics_map() + expect_s3_class(th$axis.line.x, "element_blank") + expect_s3_class(th$axis.text, "element_blank") + expect_s3_class(th$axis.ticks, "element_blank") + expect_s3_class(th$axis.title.x, "element_blank") + expect_s3_class(th$axis.title.y, "element_blank") + expect_s3_class(th$panel.grid.major.x, "element_blank") + expect_s3_class(th$panel.grid.major.y, "element_blank") +}) + +test_that("theme_civilytics_map preserves title and caption elements", { + th <- theme_civilytics_map() + expect_s3_class(th$plot.title, "element_text") + expect_s3_class(th$plot.subtitle, "element_text") + expect_s3_class(th$plot.caption, "element_text") +}) + +test_that("theme_civilytics_map applies to a ggplot without error", { + p <- ggplot(mpg, aes(displ, hwy)) + + geom_point() + + theme_civilytics_map() + expect_no_error(ggplot_build(p)) +}) + +# --- theme_civilytics_dark_map ----------------------------------------------- + +test_that("theme_civilytics_dark_map returns a complete theme", { + th <- theme_civilytics_dark_map() + expect_s3_class(th, "theme") + expect_true(attr(th, "complete")) +}) + +test_that("theme_civilytics_dark_map suppresses axes on dark background", { + th <- theme_civilytics_dark_map() + expect_s3_class(th$axis.line.x, "element_blank") + expect_s3_class(th$axis.text, "element_blank") + expect_s3_class(th$axis.ticks, "element_blank") + expect_equal(th$plot.background$fill, unname(civilytics_colors["navy_700"])) +}) + +test_that("theme_civilytics_dark_map applies to a ggplot without error", { + p <- ggplot(mpg, aes(displ, hwy)) + + geom_point() + + theme_civilytics_dark_map() + expect_no_error(ggplot_build(p)) +}) + +# --- theme_civilytics_slide_map ---------------------------------------------- + +test_that("theme_civilytics_slide_map returns a complete theme", { + th <- theme_civilytics_slide_map() + expect_s3_class(th, "theme") + expect_true(attr(th, "complete")) +}) + +test_that("theme_civilytics_slide_map has larger font and suppressed axes", { + th <- theme_civilytics_slide_map() + th_base <- theme_civilytics_map() + expect_gt(th$text$size, th_base$text$size) + expect_s3_class(th$axis.line.x, "element_blank") + expect_s3_class(th$axis.text, "element_blank") +}) + +test_that("theme_civilytics_slide_map has transparent background by default", { + th <- theme_civilytics_slide_map() + expect_true(is.na(th$plot.background$fill)) +}) + +test_that("theme_civilytics_slide_map applies to a ggplot without error", { + p <- ggplot(mpg, aes(displ, hwy)) + + geom_point() + + theme_civilytics_slide_map() + expect_no_error(ggplot_build(p)) +}) + # --- make_logo_grob ---------------------------------------------------------- test_that("make_logo_grob returns a gg object for all type/variant combos", {