Author SHA1 Message Date
kodor 967e7e1111 docs: regenerate roxygen documentation for paper_bg and force params (#19)
R-CMD-check / R CMD check (pull_request) Successful in 4m29s
2026-08-01 21:44:50 -04:00
kodor f32093c662 fix(test): update theme tests for paper_bg=FALSE default (#19)
R-CMD-check / R CMD check (pull_request) Failing after 5m47s
2026-08-01 21:29:44 -04:00
kodor e257180f15 fix: resolve locked namespace binding in civilytics_load_fonts() and default theme to transparent background
R-CMD-check / R CMD check (pull_request) Failing after 4m13s
- Replace .cv_fonts_loaded <<- TRUE with environment-based state
  (.cv_state) to avoid 'locked namespace binding' error (#18)
- Add force=FALSE argument for idempotent font loading with guard check
- Flip theme_civilytics() paper_bg default to FALSE (transparent) (#7)
- Update roxygen docs and examples for both changes
2026-08-01 02:07:09 -04:00
jared 0ef0f310e2 Merge pull request 'fix(brand): use real company name "Civilytics Consulting" in templates' (#17) from fix/brand-name-consulting into master
R-CMD-check / R CMD check (push) Successful in 4m28s
2026-07-09 16:42:18 -04:00
jared 4fb5c488f8 fix(brand): use real company name "Civilytics Consulting" in templates
R-CMD-check / R CMD check (pull_request) Successful in 4m27s
The title-page kicker in the Typst and LaTeX templates was hardcoded to
"Civilytics Research", which is not a real entity — the company is
Civilytics Consulting. Fix the kicker in both templates, and update the
example report/slides that echoed the same non-real name. Bump to 0.3.1.
2026-07-09 16:41:55 -04:00
jared 96dc1c4c6b Merge pull request 'feat(flextable): reusable Civilytics flextable branding helpers' (#16) from feat/flextable-branding into master
R-CMD-check / R CMD check (push) Successful in 4m25s
2026-07-09 16:18:43 -04:00
jared 4f51a49903 feat(flextable): add reusable Civilytics flextable branding helpers
R-CMD-check / R CMD check (pull_request) Successful in 4m20s
Generalize the flextable brand styling and "export to PNG + stamp logo"
pattern repeated across Civilytics projects into two small functions:

- style_flextable_civilytics(): applies only the visual brand (header
  fill/color, body font/size, zebra striping guarded for <2 rows,
  borders, footer styling, fixed layout) to an already-structured
  flextable. Every value is an overridable parameter; zebra toggles
  striping. Structure (labels, headers, widths, alignment, footer text)
  stays with the caller.
- save_branded_flextable_png(): exports a styled flextable to PNG via
  ragg::agg_png() sized to the table plus extra_height headroom, then
  optionally stamps the logo via stamp_logo_png() (... forwarded).

flextable, officer, and ragg are added to Suggests (not Imports) to keep
the base install light; both functions reference them fully qualified and
guard with requireNamespace() + an install hint. Real round-trip tests
cover styling, 1-row idempotence, PNG export, and extra_height.
2026-06-22 16:53:37 -04:00
20 changed files with 514 additions and 61 deletions
+5 -2
View File
@@ -1,7 +1,7 @@
Package: civilytics Package: civilytics
Type: Package Type: Package
Title: Brand Themes, Color Palettes, and Utility Functions for Civilytics Title: Brand Themes, Color Palettes, and Utility Functions for Civilytics
Version: 0.3.0 Version: 0.3.1
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"))
@@ -31,7 +31,10 @@ Encoding: UTF-8
Suggests: Suggests:
testthat (>= 3.0.0), testthat (>= 3.0.0),
tidycensus, tidycensus,
quarto quarto,
flextable,
officer,
ragg
Config/testthat/edition: 3 Config/testthat/edition: 3
Config/roxygen2/version: 8.0.0 Config/roxygen2/version: 8.0.0
RoxygenNote: 7.3.3 RoxygenNote: 7.3.3
+2
View File
@@ -39,11 +39,13 @@ export(rnh)
export(round_to_nearest_half) export(round_to_nearest_half)
export(safe_max) export(safe_max)
export(safe_ratio) export(safe_ratio)
export(save_branded_flextable_png)
export(scale_color_civilytics) export(scale_color_civilytics)
export(scale_fill_civilytics) export(scale_fill_civilytics)
export(simpleCap) export(simpleCap)
export(stamp_logo_png) export(stamp_logo_png)
export(star_subs) export(star_subs)
export(style_flextable_civilytics)
export(theme_civilytics) export(theme_civilytics)
export(theme_civilytics_dark) export(theme_civilytics_dark)
export(theme_civilytics_dark_map) export(theme_civilytics_dark_map)
+175
View File
@@ -0,0 +1,175 @@
#' Apply the Civilytics brand styling to a flextable
#'
#' Applies *only* the visual Civilytics brand to an already-structured
#' [flextable::flextable()] — header fill and color, body font and size,
#' zebra striping, borders, footer styling, and a fixed table layout. The
#' caller remains responsible for table *structure*: labels
#' ([flextable::set_header_labels()]), header/title lines
#' ([flextable::add_header_lines()]), column widths ([flextable::width()]),
#' alignment ([flextable::align()]), and footer text
#' ([flextable::add_footer_lines()]). This separation keeps styling reusable
#' across projects while leaving content decisions where they belong.
#'
#' `flextable`, `officer`, and `ragg` are Suggested (not Imported) to keep the
#' base install light, so this function errors with an install hint if
#' `flextable` is unavailable.
#'
#' @param ft A [flextable::flextable()] object.
#' @param header_bg Character. Header background fill. Default `"#2c3e50"`.
#' @param header_color Character. Header text color. Default `"white"`.
#' @param title_fontsize Numeric. Font size for the first header line (the
#' title row, `i = 1`). Default `14`.
#' @param body_fontsize Numeric. Body font size. Default `11`.
#' @param font_name Character. Font family applied to all parts. Default
#' `"Arial"`.
#' @param zebra Logical. Apply alternating-row striping to even body rows.
#' Default `TRUE`. Safely skipped for tables with fewer than two body rows.
#' @param zebra_bg Character. Fill color for striped (even) body rows.
#' Default `"#f0f0eb"`.
#' @param outer_border_color Character. Outer border color. Default
#' `"#888888"`.
#' @param outer_border_width Numeric. Outer border width. Default `1`.
#' @param inner_border_color Character. Inner horizontal border color (body).
#' Default `"#cccccc"`.
#' @param inner_border_width Numeric. Inner horizontal border width. Default
#' `0.5`.
#' @param footer_fontsize Numeric. Footer font size (applied only if a footer
#' part exists). Default `9`.
#' @param footer_color Character. Footer text color. Default `"#555555"`.
#'
#' @return The styled [flextable::flextable()] object.
#' @export
#' @seealso [save_branded_flextable_png()] to export the styled table to a
#' logo-stamped PNG.
#' @examples
#' \dontrun{
#' library(flextable)
#' ft <- flextable(head(mtcars)) |>
#' add_header_lines("Motor Trend Cars") |>
#' add_footer_lines("Source: mtcars") |>
#' style_flextable_civilytics()
#' }
style_flextable_civilytics <- function(ft,
header_bg = "#2c3e50",
header_color = "white",
title_fontsize = 14,
body_fontsize = 11,
font_name = "Arial",
zebra = TRUE,
zebra_bg = "#f0f0eb",
outer_border_color = "#888888",
outer_border_width = 1,
inner_border_color = "#cccccc",
inner_border_width = 0.5,
footer_fontsize = 9,
footer_color = "#555555") {
if (!requireNamespace("flextable", quietly = TRUE)) {
stop("Install 'flextable' to use this function.", call. = FALSE)
}
if (!requireNamespace("officer", quietly = TRUE)) {
stop("Install 'officer' to use this function.", call. = FALSE)
}
# Header: navy fill, white bold text, larger title line.
ft <- flextable::bg(ft, bg = header_bg, part = "header")
ft <- flextable::color(ft, color = header_color, part = "header")
ft <- flextable::bold(ft, part = "header")
ft <- flextable::fontsize(ft, i = 1, size = title_fontsize, part = "header")
# Body: readable size, brand font across all parts.
ft <- flextable::fontsize(ft, size = body_fontsize, part = "body")
ft <- flextable::font(ft, fontname = font_name, part = "all")
# Zebra striping on even body rows, guarded for tables with < 2 rows.
nrow_body <- flextable::nrow_part(ft, part = "body")
if (zebra && nrow_body >= 2) {
ft <- flextable::bg(ft, i = seq(2, nrow_body, 2), bg = zebra_bg,
part = "body")
}
# Borders: outer frame on all parts, light horizontal rules in the body.
ft <- flextable::border_outer(
ft,
border = officer::fp_border(color = outer_border_color,
width = outer_border_width),
part = "all"
)
ft <- flextable::border_inner_h(
ft,
border = officer::fp_border(color = inner_border_color,
width = inner_border_width),
part = "body"
)
# Footer styling — only if the table actually has a footer part.
if (flextable::nrow_part(ft, part = "footer") > 0) {
ft <- flextable::fontsize(ft, size = footer_fontsize, part = "footer")
ft <- flextable::color(ft, color = footer_color, part = "footer")
}
flextable::set_table_properties(ft, layout = "fixed")
}
#' Save a branded flextable to a logo-stamped PNG
#'
#' Exports a styled [flextable::flextable()] to a PNG via
#' [ragg::agg_png()] (sized to the table's own dimensions plus a little
#' headroom for the logo), then optionally stamps the Civilytics logo onto the
#' file with [stamp_logo_png()] so file-based tables stay visually consistent
#' with [civilytics_logo()]-branded plots.
#'
#' `ragg` and `flextable` are Suggested (not Imported); this function errors
#' with an install hint if either is unavailable.
#'
#' @param ft A [flextable::flextable()] object, typically already styled with
#' [style_flextable_civilytics()].
#' @param path Character. Output PNG path. Returned invisibly.
#' @param logo Logical. Stamp the Civilytics logo onto the saved PNG via
#' [stamp_logo_png()]. Default `TRUE`.
#' @param res Numeric. Output resolution in PPI passed to [ragg::agg_png()].
#' Default `300`.
#' @param extra_height Numeric. Additional height in inches added to the
#' table's natural height to leave room for the stamped logo. Default `0.4`.
#' @param ... Additional arguments forwarded to [stamp_logo_png()] (e.g.
#' `type`, `variant`, `position`, `width_frac`, `margin_frac`).
#'
#' @return `path`, invisibly.
#' @export
#' @seealso [style_flextable_civilytics()] to apply the brand styling, and
#' [stamp_logo_png()] for the underlying logo compositing.
#' @examples
#' \dontrun{
#' library(flextable)
#' ft <- flextable(head(mtcars)) |>
#' add_footer_lines("Source: mtcars") |>
#' style_flextable_civilytics()
#' save_branded_flextable_png(ft, "table.png")
#' save_branded_flextable_png(ft, "table.png", position = "bottom-left")
#' knitr::include_graphics("table.png")
#' }
save_branded_flextable_png <- function(ft, path, logo = TRUE, res = 300,
extra_height = 0.4, ...) {
if (!requireNamespace("flextable", quietly = TRUE)) {
stop("Install 'flextable' to use this function.", call. = FALSE)
}
if (!requireNamespace("ragg", quietly = TRUE)) {
stop("Install 'ragg' to use this function.", call. = FALSE)
}
d <- flextable::flextable_dim(ft)
ragg::agg_png(path, width = d$width, height = d$height + extra_height,
units = "in", res = res)
on.exit(grDevices::dev.off(), add = TRUE)
plot(ft)
if (logo) {
# dev.off() must run before stamp_logo_png() re-reads the file. Flush the
# device now and clear the on.exit handler so it does not fire twice.
grDevices::dev.off()
on.exit()
stamp_logo_png(path, ...)
}
invisible(path)
}
+24 -11
View File
@@ -5,32 +5,45 @@ CV_FONT_SANS <- "Inter" # axis text, legends, UI elements
CV_FONT_SERIF <- "Source Serif 4" # body prose / editorial long-form CV_FONT_SERIF <- "Source Serif 4" # body prose / editorial long-form
CV_FONT_MONO <- "JetBrains Mono" # code, data tables, numeric callouts CV_FONT_MONO <- "JetBrains Mono" # code, data tables, numeric callouts
# Internal flag so civilytics_load_fonts() is idempotent within a session. # Mutable package state held in an environment so the binding itself stays
.cv_fonts_loaded <- FALSE # locked (R locks all namespace bindings at load time) while the contents
# remain writable. See https://adv-r.hadley.nz/environments.html#environments-as-containers
.cv_state <- new.env(parent = emptyenv())
.cv_state$fonts_loaded <- FALSE
#' Load Civilytics brand fonts #' Load Civilytics brand fonts
#' #'
#' Downloads Inter and Libre Franklin from Google Fonts via #' Downloads Inter, Libre Franklin, Source Serif 4 and JetBrains Mono from
#' [sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so #' Google Fonts via [sysfonts::font_add_google()], then calls
#' that all graphics devices render text with those fonts. This is called #' [showtext::showtext_auto()] so that all graphics devices render text with
#' automatically when the package loads; use this function to retry if the #' those fonts. This is called automatically when the package loads; use this
#' initial load failed (e.g., the machine was offline at load time). #' function to retry if the initial load failed (e.g., the machine was offline
#' at load time).
#' #'
#' @return Invisibly returns `NULL`. #' Subsequent calls within the same session are no-ops unless `force = TRUE`.
#'
#' @param force Logical. If `TRUE`, reload fonts even if they were already
#' loaded in this session. Default `FALSE`.
#'
#' @return Invisibly returns `TRUE` if fonts were loaded, `FALSE` if skipped
#' (already loaded and `force = FALSE`).
#' @export #' @export
#' #'
#' @examples #' @examples
#' \dontrun{ #' \dontrun{
#' civilytics_load_fonts() #' civilytics_load_fonts()
#' } #' }
civilytics_load_fonts <- function() { civilytics_load_fonts <- function(force = FALSE) {
if (.cv_state$fonts_loaded && !force) return(invisible(FALSE))
sysfonts::font_add_google("Inter", family = "Inter") sysfonts::font_add_google("Inter", family = "Inter")
sysfonts::font_add_google("Libre Franklin", family = "Libre Franklin") sysfonts::font_add_google("Libre Franklin", family = "Libre Franklin")
sysfonts::font_add_google("Source Serif 4", family = "Source Serif 4") sysfonts::font_add_google("Source Serif 4", family = "Source Serif 4")
sysfonts::font_add_google("JetBrains Mono", family = "JetBrains Mono") sysfonts::font_add_google("JetBrains Mono", family = "JetBrains Mono")
showtext::showtext_auto() showtext::showtext_auto()
.cv_fonts_loaded <<- TRUE
invisible(NULL) .cv_state$fonts_loaded <- TRUE
invisible(TRUE)
} }
.onLoad <- function(libname, pkgname) { .onLoad <- function(libname, pkgname) {
+10 -8
View File
@@ -36,10 +36,12 @@
#' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`). #' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).
#' @param grid Character. Which major gridlines to draw: `"y"` (default, #' @param grid Character. Which major gridlines to draw: `"y"` (default,
#' horizontal only), `"x"` (vertical only), `"both"`, or `"none"`. #' horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.
#' @param paper_bg Logical. If `TRUE` (default), fill the plot and panel #' @param paper_bg Logical. If `TRUE`, fill the plot and panel backgrounds
#' backgrounds with the warm `paper` color. Set to `FALSE` for a #' with the warm `paper` color (Civilytics cream). Default is `FALSE`
#' transparent background (useful for slides or overlay on colored #' (transparent) so that figures composite cleanly onto any background.
#' surfaces). #' Set to `TRUE` for the branded cream canvas. Note: a transparent device
#' background (e.g., `dev = "ragg_png"`, `dev.args = list(background =
#' "transparent")`) is also needed for fully-transparent PNG exports.
#' #'
#' @section Font size hierarchy: #' @section Font size hierarchy:
#' All text sizes are derived from `font_size` using relative scale factors. #' All text sizes are derived from `font_size` using relative scale factors.
@@ -68,7 +70,7 @@
#' \dontrun{ #' \dontrun{
#' library(ggplot2) #' library(ggplot2)
#' #'
#' # Default editorial theme #' # Default — transparent background for embedding
#' ggplot(mpg, aes(displ, hwy)) + #' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() + #' geom_point() +
#' theme_civilytics() #' theme_civilytics()
@@ -79,10 +81,10 @@
#' scale_color_civilytics() + #' scale_color_civilytics() +
#' theme_civilytics(grid = "both") #' theme_civilytics(grid = "both")
#' #'
#' # Transparent background for embedding #' # Branded cream background (opt-in)
#' ggplot(mpg, aes(displ, hwy)) + #' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() + #' geom_point() +
#' theme_civilytics(paper_bg = FALSE) #' theme_civilytics(paper_bg = TRUE)
#' #'
#' # Larger text for poster or display #' # Larger text for poster or display
#' ggplot(mpg, aes(displ, hwy)) + #' ggplot(mpg, aes(displ, hwy)) +
@@ -102,7 +104,7 @@ theme_civilytics <- function(
accent = unname(civilytics_colors["ember_600"]), accent = unname(civilytics_colors["ember_600"]),
strip_color = unname(civilytics_colors["paper_2"]), strip_color = unname(civilytics_colors["paper_2"]),
grid = c("y", "x", "both", "none"), grid = c("y", "x", "both", "none"),
paper_bg = TRUE) { paper_bg = FALSE) {
grid <- match.arg(grid) grid <- match.arg(grid)
half_line <- font_size / 2 half_line <- font_size / 2
+1 -1
View File
@@ -2,7 +2,7 @@
title: "Who pays when rent outpaces wages?" title: "Who pays when rent outpaces wages?"
subtitle: "A 12-county analysis of cost-burdened renter households, 2019–2024." subtitle: "A 12-county analysis of cost-burdened renter households, 2019–2024."
author: author:
- name: "Civilytics Research" - name: "Jared Knowles"
affiliation: "Civilytics Consulting" affiliation: "Civilytics Consulting"
date: "2026-04-15" date: "2026-04-15"
abstract: | abstract: |
+1 -1
View File
@@ -55,7 +55,7 @@ Big idea goes here.
> Rent has outpaced wages in every county we studied. > Rent has outpaced wages in every county we studied.
Civilytics Research, 2026 Civilytics Consulting, 2026
## Thank you {.thank-you} ## Thank you {.thank-you}
+1 -1
View File
@@ -11,7 +11,7 @@
\pagecolor{paper} \pagecolor{paper}
\color{ink} \color{ink}
\vspace*{0.5in} \vspace*{0.5in}
{\sffamily\bfseries\scriptsize\color{ember}\MakeUppercase{— Civilytics Research}\par} {\sffamily\bfseries\scriptsize\color{ember}\MakeUppercase{— Civilytics Consulting}\par}
\vspace{12pt} \vspace{12pt}
{\displayfont\fontsize{32pt}{34pt}\selectfont\bfseries\color{ink}\thetitle\par} {\displayfont\fontsize{32pt}{34pt}\selectfont\bfseries\color{ink}\thetitle\par}
\vspace{8pt} \vspace{8pt}
+1 -1
View File
@@ -164,7 +164,7 @@
if title != none { if title != none {
block[ block[
#set text(font: sans-stack, size: 8pt, weight: 600, fill: ember, tracking: 0.1em) #set text(font: sans-stack, size: 8pt, weight: 600, fill: ember, tracking: 0.1em)
#upper[— Civilytics Research] #upper[— Civilytics Consulting]
] ]
v(8pt) v(8pt)
block[ block[
+15 -7
View File
@@ -4,17 +4,25 @@
\alias{civilytics_load_fonts} \alias{civilytics_load_fonts}
\title{Load Civilytics brand fonts} \title{Load Civilytics brand fonts}
\usage{ \usage{
civilytics_load_fonts() civilytics_load_fonts(force = FALSE)
}
\arguments{
\item{force}{Logical. If \code{TRUE}, reload fonts even if they were already
loaded in this session. Default \code{FALSE}.}
} }
\value{ \value{
Invisibly returns `NULL`. Invisibly returns \code{TRUE} if fonts were loaded, \code{FALSE} if skipped
(already loaded and \code{force = FALSE}).
} }
\description{ \description{
Downloads Inter and Libre Franklin from Google Fonts via Downloads Inter, Libre Franklin, Source Serif 4 and JetBrains Mono from
[sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so Google Fonts via \code{sysfonts::font_add_google()}, then calls
that all graphics devices render text with those fonts. This is called \code{showtext::showtext_auto()} so that all graphics devices render text with
automatically when the package loads; use this function to retry if the those fonts. This is called automatically when the package loads; use this
initial load failed (e.g., the machine was offline at load time). function to retry if the initial load failed (e.g., the machine was offline
at load time).
Subsequent calls within the same session are no-ops unless \code{force = TRUE}.
} }
\examples{ \examples{
\dontrun{ \dontrun{
+62
View File
@@ -0,0 +1,62 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/flextable.R
\name{save_branded_flextable_png}
\alias{save_branded_flextable_png}
\title{Save a branded flextable to a logo-stamped PNG}
\usage{
save_branded_flextable_png(
ft,
path,
logo = TRUE,
res = 300,
extra_height = 0.4,
...
)
}
\arguments{
\item{ft}{A [flextable::flextable()] object, typically already styled with
[style_flextable_civilytics()].}
\item{path}{Character. Output PNG path. Returned invisibly.}
\item{logo}{Logical. Stamp the Civilytics logo onto the saved PNG via
[stamp_logo_png()]. Default `TRUE`.}
\item{res}{Numeric. Output resolution in PPI passed to [ragg::agg_png()].
Default `300`.}
\item{extra_height}{Numeric. Additional height in inches added to the
table's natural height to leave room for the stamped logo. Default `0.4`.}
\item{...}{Additional arguments forwarded to [stamp_logo_png()] (e.g.
`type`, `variant`, `position`, `width_frac`, `margin_frac`).}
}
\value{
`path`, invisibly.
}
\description{
Exports a styled [flextable::flextable()] to a PNG via
[ragg::agg_png()] (sized to the table's own dimensions plus a little
headroom for the logo), then optionally stamps the Civilytics logo onto the
file with [stamp_logo_png()] so file-based tables stay visually consistent
with [civilytics_logo()]-branded plots.
}
\details{
`ragg` and `flextable` are Suggested (not Imported); this function errors
with an install hint if either is unavailable.
}
\examples{
\dontrun{
library(flextable)
ft <- flextable(head(mtcars)) |>
add_footer_lines("Source: mtcars") |>
style_flextable_civilytics()
save_branded_flextable_png(ft, "table.png")
save_branded_flextable_png(ft, "table.png", position = "bottom-left")
knitr::include_graphics("table.png")
}
}
\seealso{
[style_flextable_civilytics()] to apply the brand styling, and
[stamp_logo_png()] for the underlying logo compositing.
}
+92
View File
@@ -0,0 +1,92 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/flextable.R
\name{style_flextable_civilytics}
\alias{style_flextable_civilytics}
\title{Apply the Civilytics brand styling to a flextable}
\usage{
style_flextable_civilytics(
ft,
header_bg = "#2c3e50",
header_color = "white",
title_fontsize = 14,
body_fontsize = 11,
font_name = "Arial",
zebra = TRUE,
zebra_bg = "#f0f0eb",
outer_border_color = "#888888",
outer_border_width = 1,
inner_border_color = "#cccccc",
inner_border_width = 0.5,
footer_fontsize = 9,
footer_color = "#555555"
)
}
\arguments{
\item{ft}{A [flextable::flextable()] object.}
\item{header_bg}{Character. Header background fill. Default `"#2c3e50"`.}
\item{header_color}{Character. Header text color. Default `"white"`.}
\item{title_fontsize}{Numeric. Font size for the first header line (the
title row, `i = 1`). Default `14`.}
\item{body_fontsize}{Numeric. Body font size. Default `11`.}
\item{font_name}{Character. Font family applied to all parts. Default
`"Arial"`.}
\item{zebra}{Logical. Apply alternating-row striping to even body rows.
Default `TRUE`. Safely skipped for tables with fewer than two body rows.}
\item{zebra_bg}{Character. Fill color for striped (even) body rows.
Default `"#f0f0eb"`.}
\item{outer_border_color}{Character. Outer border color. Default
`"#888888"`.}
\item{outer_border_width}{Numeric. Outer border width. Default `1`.}
\item{inner_border_color}{Character. Inner horizontal border color (body).
Default `"#cccccc"`.}
\item{inner_border_width}{Numeric. Inner horizontal border width. Default
`0.5`.}
\item{footer_fontsize}{Numeric. Footer font size (applied only if a footer
part exists). Default `9`.}
\item{footer_color}{Character. Footer text color. Default `"#555555"`.}
}
\value{
The styled [flextable::flextable()] object.
}
\description{
Applies *only* the visual Civilytics brand to an already-structured
[flextable::flextable()] — header fill and color, body font and size,
zebra striping, borders, footer styling, and a fixed table layout. The
caller remains responsible for table *structure*: labels
([flextable::set_header_labels()]), header/title lines
([flextable::add_header_lines()]), column widths ([flextable::width()]),
alignment ([flextable::align()]), and footer text
([flextable::add_footer_lines()]). This separation keeps styling reusable
across projects while leaving content decisions where they belong.
}
\details{
`flextable`, `officer`, and `ragg` are Suggested (not Imported) to keep the
base install light, so this function errors with an install hint if
`flextable` is unavailable.
}
\examples{
\dontrun{
library(flextable)
ft <- flextable(head(mtcars)) |>
add_header_lines("Motor Trend Cars") |>
add_footer_lines("Source: mtcars") |>
style_flextable_civilytics()
}
}
\seealso{
[save_branded_flextable_png()] to export the styled table to a
logo-stamped PNG.
}
+10 -8
View File
@@ -17,7 +17,7 @@ theme_civilytics(
accent = unname(civilytics_colors["ember_600"]), accent = unname(civilytics_colors["ember_600"]),
strip_color = unname(civilytics_colors["paper_2"]), strip_color = unname(civilytics_colors["paper_2"]),
grid = c("y", "x", "both", "none"), grid = c("y", "x", "both", "none"),
paper_bg = TRUE paper_bg = FALSE
) )
} }
\arguments{ \arguments{
@@ -56,10 +56,12 @@ to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{grid}{Character. Which major gridlines to draw: `"y"` (default, \item{grid}{Character. Which major gridlines to draw: `"y"` (default,
horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.} horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas. Note: a transparent device
background (e.g., `dev = "ragg_png"`, `dev.args = list(background =
"transparent")`) is also needed for fully-transparent PNG exports.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
@@ -105,7 +107,7 @@ shrinkage.
\dontrun{ \dontrun{
library(ggplot2) library(ggplot2)
# Default editorial theme # Default — transparent background for embedding
ggplot(mpg, aes(displ, hwy)) + ggplot(mpg, aes(displ, hwy)) +
geom_point() + geom_point() +
theme_civilytics() theme_civilytics()
@@ -116,10 +118,10 @@ ggplot(mpg, aes(displ, hwy, colour = class)) +
scale_color_civilytics() + scale_color_civilytics() +
theme_civilytics(grid = "both") theme_civilytics(grid = "both")
# Transparent background for embedding # Branded cream background (opt-in)
ggplot(mpg, aes(displ, hwy)) + ggplot(mpg, aes(displ, hwy)) +
geom_point() + geom_point() +
theme_civilytics(paper_bg = FALSE) theme_civilytics(paper_bg = TRUE)
# Larger text for poster or display # Larger text for poster or display
ggplot(mpg, aes(displ, hwy)) + ggplot(mpg, aes(displ, hwy)) +
+4 -4
View File
@@ -56,10 +56,10 @@ to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{grid}{Character. Which major gridlines to draw: `"y"` (default, \item{grid}{Character. Which major gridlines to draw: `"y"` (default,
horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.} horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+4 -4
View File
@@ -52,10 +52,10 @@ design system.}
\item{strip_color}{Character. Hex code for facet strip background. Defaults \item{strip_color}{Character. Hex code for facet strip background. Defaults
to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+4 -4
View File
@@ -52,10 +52,10 @@ design system.}
\item{strip_color}{Character. Hex code for facet strip background. Defaults \item{strip_color}{Character. Hex code for facet strip background. Defaults
to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+4 -4
View File
@@ -56,10 +56,10 @@ to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{grid}{Character. Which major gridlines to draw: `"y"` (default, \item{grid}{Character. Which major gridlines to draw: `"y"` (default,
horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.} horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+4 -4
View File
@@ -52,10 +52,10 @@ design system.}
\item{strip_color}{Character. Hex code for facet strip background. Defaults \item{strip_color}{Character. Hex code for facet strip background. Defaults
to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).}
\item{paper_bg}{Logical. If `TRUE` (default), fill the plot and panel \item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds
backgrounds with the warm `paper` color. Set to `FALSE` for a with the warm `paper` color (Civilytics cream). Default is `FALSE`
transparent background (useful for slides or overlay on colored (transparent) so that figures composite cleanly onto any background.
surfaces).} Set to `TRUE` for the branded cream canvas.}
} }
\value{ \value{
A complete ggplot2 [ggplot2::theme()] object. A complete ggplot2 [ggplot2::theme()] object.
+89
View File
@@ -0,0 +1,89 @@
test_that("style_flextable_civilytics returns a styled flextable", {
skip_if_not_installed("flextable")
skip_if_not_installed("officer")
ft <- flextable::flextable(head(mtcars, 4))
ft <- flextable::add_footer_lines(ft, "Source: mtcars")
styled <- style_flextable_civilytics(ft)
expect_s3_class(styled, "flextable")
# Fixed layout is the documented end-state of the styling.
expect_identical(styled$properties$layout, "fixed")
})
test_that("style_flextable_civilytics is idempotent on a 1-row table (no zebra error)", {
skip_if_not_installed("flextable")
skip_if_not_installed("officer")
ft <- flextable::flextable(head(mtcars, 1))
expect_no_error(once <- style_flextable_civilytics(ft))
# Re-styling an already-styled table must not error either.
expect_no_error(twice <- style_flextable_civilytics(once))
expect_s3_class(twice, "flextable")
})
test_that("zebra = FALSE still returns a valid flextable", {
skip_if_not_installed("flextable")
skip_if_not_installed("officer")
ft <- flextable::flextable(head(mtcars, 6))
expect_s3_class(style_flextable_civilytics(ft, zebra = FALSE), "flextable")
})
test_that("save_branded_flextable_png writes a PNG with plausible dimensions", {
skip_if_not_installed("flextable")
skip_if_not_installed("officer")
skip_if_not_installed("ragg")
skip_if_not_installed("png")
ft <- flextable::flextable(head(mtcars, 4))
ft <- flextable::add_footer_lines(ft, "Source: mtcars")
ft <- style_flextable_civilytics(ft)
path <- tempfile(fileext = ".png")
on.exit(unlink(path), add = TRUE)
out <- save_branded_flextable_png(ft, path)
expect_identical(out, path)
expect_true(file.exists(path))
dims <- dim(png::readPNG(path))
# height x width x channels — a real table is at least a few hundred px each.
expect_gt(dims[1], 50)
expect_gt(dims[2], 50)
})
test_that("save_branded_flextable_png with logo = FALSE skips stamping but still writes", {
skip_if_not_installed("flextable")
skip_if_not_installed("officer")
skip_if_not_installed("ragg")
skip_if_not_installed("png")
ft <- style_flextable_civilytics(flextable::flextable(head(mtcars, 3)))
path <- tempfile(fileext = ".png")
on.exit(unlink(path), add = TRUE)
save_branded_flextable_png(ft, path, logo = FALSE)
expect_true(file.exists(path))
expect_gt(file.info(path)$size, 0)
})
test_that("extra_height increases the exported PNG height", {
skip_if_not_installed("flextable")
skip_if_not_installed("officer")
skip_if_not_installed("ragg")
skip_if_not_installed("png")
ft <- style_flextable_civilytics(flextable::flextable(head(mtcars, 4)))
path_small <- tempfile(fileext = ".png")
path_big <- tempfile(fileext = ".png")
on.exit(unlink(c(path_small, path_big)), add = TRUE)
save_branded_flextable_png(ft, path_small, logo = FALSE, extra_height = 0)
save_branded_flextable_png(ft, path_big, logo = FALSE, extra_height = 2)
expect_gt(dim(png::readPNG(path_big))[1], dim(png::readPNG(path_small))[1])
})
+6 -1
View File
@@ -133,8 +133,13 @@ test_that("theme_civilytics uses brand ink color for text", {
expect_equal(th$text$colour, unname(civilytics_colors["ink"])) expect_equal(th$text$colour, unname(civilytics_colors["ink"]))
}) })
test_that("theme_civilytics uses brand paper color for plot background", { test_that("theme_civilytics has transparent background by default", {
th <- theme_civilytics() th <- theme_civilytics()
expect_true(is.na(th$plot.background$fill))
})
test_that("theme_civilytics uses brand paper color when paper_bg = TRUE", {
th <- theme_civilytics(paper_bg = TRUE)
expect_equal(th$plot.background$fill, unname(civilytics_colors["paper"])) expect_equal(th$plot.background$fill, unname(civilytics_colors["paper"]))
}) })