Author SHA1 Message Date
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 6ea9265236 Merge pull request 'fix(quarto): repair LaTeX + Typst branded templates (#11 #12 #13 #14)' (#15) from fix/quarto-template-bugs into master
R-CMD-check / R CMD check (push) Successful in 4m13s
2026-07-09 16:18:29 -04:00
jared b9d427eb76 fix(typst): ship template as partials so code blocks render (#12)
R-CMD-check / R CMD check (pull_request) Successful in 3m54s
The single-file `template: civilytics-typst.typ` discarded Quarto's
auto-generated `definitions` partial, so any Typst document containing a
code block failed with `unknown variable: Skylighting`.

Ship the template as Quarto template-partials instead, so Quarto keeps its
definitions (Skylighting + token functions) and syntax-highlighted code
blocks render while the Civilytics branding still applies:

- split civilytics-typst.typ into typst-template.typ (the styling function)
  and typst-show.typ (the show/entry point, keyword-safe [ ] wrapping kept);
  remove the single-file template
- use_civilytics_theme() now copies both partials and prints the
  template-partials usage
- example report.qmd uses template-partials

Verified: a report with an R code block renders with working syntax
highlighting and full branding.
2026-07-09 16:10:15 -04:00
jared a904d2313a fix(quarto): repair LaTeX + Typst branded templates
R-CMD-check / R CMD check (pull_request) Successful in 3m56s
- LaTeX: capture pandoc's \subtitle into \thesubtitle so subtitled PDFs
  compile; suppress the default \maketitle/abstract so only the branded
  title page renders (no double title); drop the unused tikz dependency
  from the title partial (#13)
- LaTeX: stop requiring a "Source Serif 4 SemiBold" face that the setup
  never installs; use the family's native Bold weight (#14)
- Typst: wrap title/subtitle/date in [ ] so text containing Typst keywords
  ("for"/"in") no longer breaks compilation (#12)
- Fix footer URL civilytics.consulting -> civilytics.com in the LaTeX and
  Typst templates and the slides example (#11)
- Bump version to 0.3.0
2026-07-09 15:57:47 -04:00
jared 761a72da18 Merge pull request 'feat(logo): add stamp_logo_png() for branding file-based (PNG) outputs' (#9) from feat/stamp-logo-png into master
R-CMD-check / R CMD check (push) Successful in 3m51s
Reviewed-on: #9
2026-06-22 16:59:52 -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
jared 5fc8155965 feat(logo): add stamp_logo_png() to brand file-based PNG outputs
R-CMD-check / R CMD check (pull_request) Successful in 3m53s
civilytics_logo()/make_logo_grob() brand ggplot/grobs, but flextables and
other outputs are rendered to PNG first and can't use them. stamp_logo_png()
is the raster analogue: it resolves the SAME brand asset that make_logo_grob()
uses (via the type/variant switch) and composites it into a corner of an
existing PNG in place, so file-based tables stay visually consistent with
logo-branded plots.

Dependency-free by design — uses only png + grid + grDevices (all already
imported), no magick. Configurable type/variant/position/width/margin;
preserves the image's pixel dimensions. Adds tests (21 assertions).
2026-06-22 16:33:08 -04:00
jared 41162597cf Merge pull request 'Fix issues #1, #3, #5: misc improvements' (#6) from fix/issues-1-3-5-misc-improvements into master
R-CMD-check / R CMD check (push) Successful in 4m25s
R-CMD-check / R CMD check (pull_request) Successful in 4m6s
Reviewed-on: #6
2026-06-04 14:04:44 -04:00
jared 2a74ec2bd3 test: add security regression test and beep(type='all') coverage
R-CMD-check / R CMD check (pull_request) Successful in 3m53s
- Add test verifying .send_webhook() and .send_desktop_notify() use
  system2() (not system()) to prevent command injection. This test
  reads the function body and asserts system2 is present and
  system("...") is absent, so the vulnerability cannot be
  reintroduced by accident.
- Add test for beep(type='all') exercising both beep and notify paths.
2026-06-04 13:48:21 +00:00
jared 366ac39fac fix: replace system() with system2() to prevent command injection in beep notifications
R-CMD-check / R CMD check (pull_request) Successful in 3m50s
Use system2() with separate args instead of shell-interpolated system()
calls in .send_desktop_notify() and .send_webhook(). This eliminates
command injection risk when msg, status, url, or payload contain shell
metacharacters.
2026-06-04 13:17:06 +00:00
jared fc8bf1866b Fix beep.Rd Rd syntax errors (remove invalid \char escape)
R-CMD-check / R CMD check (pull_request) Successful in 4m44s
2026-06-03 19:11:19 +00:00
jared 75b9a73165 Fix CI warnings: add jsonlite dep, flush.console import, Rd docs
R-CMD-check / R CMD check (pull_request) Failing after 1m13s
- Add jsonlite to Imports (used by beep() webhook)
- Add importFrom(utils, flush.console) to NAMESPACE
- Update na_sum.Rd to include quiet parameter
- Create beep.Rd documentation
2026-06-03 19:08:48 +00:00
jared 2f1cdcde52 Fix issues #1, #3, #5: misc improvements
R-CMD-check / R CMD check (pull_request) Failing after 4m43s
- #5: Unwrap _brand.yml for Quarto 1.9+ compatibility (remove top-level
  brand: wrapper so meta/logo/color/typography are at the top level)
- #1: Add quiet parameter to na_sum() to suppress warnings in loops/pipelines
- #3: Add beep() function for CLI beep, desktop notifications, and webhook
  alerts (supports type=beep|notify|webhook|all)
2026-06-03 19:01:30 +00:00
jared 136c53643c feat: add Quarto themes and templates (revealjs, HTML, PDF, Typst)
R-CMD-check / R CMD check (push) Successful in 3m52s
Integrate Civilytics Reveal.js slide extension, HTML/PDF/Typst document
themes, and _brand.yml into the package under inst/quarto/. Three new
helper functions (use_civilytics_revealjs, use_civilytics_theme,
use_civilytics_brand) copy assets into a user's Quarto project. Logos
are stored once in inst/img/ and distributed at install time.
2026-05-20 09:39:27 -06:00
jared 41fe2b6170 fix: use full-bleed background rect instead of opaque logo grob
R-CMD-check / R CMD check (push) Successful in 4m4s
The previous fix applied the plot's background to the logo grob
directly, which made it opaque and covered caption/axis text that
overlaps into the logo area via negative margins.

Instead, wrap the entire arrangeGrob composition in a grobTree with
a background rectGrob behind it. This fills transparent areas (logo
strip, padding gaps) with the plot's background color while keeping
the logo grob itself transparent so overlapping text remains visible.
2026-05-19 22:12:03 -06:00
jared ac8e3eb60e fix: inherit plot background color in logo compositing
R-CMD-check / R CMD check (push) Successful in 3m41s
The logo grob uses theme_void() (transparent background), so when
arrangeGrob() composites it with a dark-themed plot, the logo strip
falls back to the device default (white). Extract the plot's
plot.background fill and apply it to the logo grob before compositing.

Fixes the issue where theme_civilytics_dark(font_size = 16) piped to
civilytics_logo(variant = "dark") would show a light background in
the logo/caption area.
2026-05-19 16:47:36 -06:00
jared 98e0949d27 feat: add map theme variants and fix dark theme text readability
R-CMD-check / R CMD check (push) Successful in 3m48s
Add theme_civilytics_map(), theme_civilytics_dark_map(), and
theme_civilytics_slide_map() for choropleths — suppress axes, ticks,
gridlines, and axis labels while preserving titles, captions, and legends.

Fix subtitle, caption, and axis text colors in theme_civilytics_dark()
which were inheriting dark ink_2/ink_3 values meant for light backgrounds,
making them hard to read on navy. Now uses navy_200/navy_300 instead.
2026-05-19 16:07:42 -06:00
jaredandClaude Opus 4.6 328d9d2fd6 feat: add position argument to civilytics_logo() for corner placement
R-CMD-check / R CMD check (push) Successful in 3m37s
Supports "bottom-right" (default), "bottom-left", "top-right", and
"top-left". The position controls both horizontal alignment of the logo
grob and whether it is placed above or below the plot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 15:44:18 -06:00
jared 95b8ba1da4 fix: revert font_size default to 14pt, keep DPI fix
R-CMD-check / R CMD check (push) Successful in 3m37s
2026-05-19 15:11:27 -06:00
jared f134621df5 fix: bump base font_size to 16pt and drop fig.retina for readable README
R-CMD-check / R CMD check (push) Successful in 3m33s
The old fig.retina=2 at 150 DPI rendered PNGs at 2x resolution that
GitHub markdown can't use, making text appear half its intended size.
Switching to 96 DPI without retina produces images at their natural
display size. Combined with the base font bump (14 → 16pt), titles
and labels now render with proper editorial presence.
2026-05-19 14:40:48 -06:00
jared 31d3e2886d feat: bump title sizes, add font_scale to logo functions, ship SVGs
R-CMD-check / R CMD check (push) Successful in 3m36s
- Change rel_large default from 16/14 to 20/14 (~1.43x) to match the
  Civilytics editorial design system. Subtitle now renders at 1.0x base
  instead of 0.86x.
- Switch axis.text to rel() sizing so all text elements cascade from the
  root font_size, enabling uniform scaling.
- Add font_scale parameter (default 1.1) to civilytics_logo(),
  add_logo(), and add_logo_ga() — inflates text by ~10% before
  arrangeGrob composition to compensate for viewport shrinkage.
- Ship SVG logo variants (mark, wordmark, pulse) in inst/img/ for
  Quarto/HTML templates.
- Add "Maintaining brand assets" section to README documenting inst/img/
  file inventory and update workflow.
- Document full font size hierarchy in theme_civilytics() roxygen.
2026-05-19 12:51:23 -06:00
jared 34622ccf88 fix: use remotes::install_git() for Gitea installation instructions
R-CMD-check / R CMD check (push) Successful in 3m37s
2026-05-19 10:46:32 -06:00
jared f9b51cc558 fix: use light-variant mark in logo example to match light logo area
R-CMD-check / R CMD check (push) Successful in 3m42s
2026-05-19 10:43:52 -06:00
jared 875251068a docs: add rendered logo examples to README gallery
R-CMD-check / R CMD check (push) Successful in 3m38s
Show three logo use cases: wordmark on a light plot, compact mark on a
dark plot, and multi-plot layout with a shared logo via add_logo_ga().
2026-05-19 10:39:12 -06:00
jared 515bae8685 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.
2026-05-19 10:35:32 -06:00
jared 099fff1d1d fix: correct pipe precedence docs — |> binds tighter than +
R-CMD-check / R CMD check (push) Successful in 2m46s
R's |> has higher precedence than +, so parentheses ARE required
around the ggplot chain before piping to civilytics_logo():

  (ggplot(df, aes(x, y)) + geom_point() + theme_civilytics()) |>
    civilytics_logo()

Updated docs, examples, and pipe test accordingly.
2026-05-17 22:54:53 -06:00
jared 7371069379 feat: rebrand logos with wordmark/mark variants and pipe-friendly API
R-CMD-check / R CMD check (push) Successful in 2m39s
- Replace old civilytics_logo.png/white.png/jpg/pdf with new rebrand
  assets: civilytics-wordmark.png, civilytics-wordmark-reverse.png,
  civilytics-mark.png, civilytics-mark-reverse.png (all transparent bg)
- make_logo_grob() now accepts type = c("wordmark", "mark") alongside
  variant = c("light", "dark") for 4 combinations
- New civilytics_logo() pipe-friendly convenience function:
    ggplot(df, aes(x, y)) + geom_point() + theme_civilytics() |>
      civilytics_logo()
  R's |> has lower precedence than +, so the full ggplot chain pipes
  through without parentheses
- Remove old icon/favicon assets no longer used
- Wrap showtext-dependent examples in \dontrun{} to prevent R CMD check
  failures in headless PostScript environments
2026-05-17 22:42:40 -06:00
jared 3a6f20b723 ci: bump rocker container to r-ver:4.6 for ggplot2 4.0 CRAN snapshot
R-CMD-check / R CMD check (push) Successful in 2m55s
2026-05-17 10:41:50 -06:00
jared c93c469267 feat: add brand color system, showtext fonts, and dark theme
R-CMD-check / R CMD check (push) Failing after 1m39s
- R/colors.R: civilytics_colors named vector (from --cv-* CSS vars on
  civilytics.com), three palettes (main/sequential/diverging),
  scale_color_civilytics() and scale_fill_civilytics() for ggplot2
- R/fonts.R: auto-loads Inter + Libre Franklin from Google Fonts via
  showtext in .onLoad(); exports civilytics_load_fonts() for manual retry
- R/theme.R: wires ggplot2 4.0 ink/paper/accent params into theme_grey()
  base, adds title_family + strip_color params; new theme_civilytics_dark()
  using navy_dark background and warm off-white text
- R/logo.R: make_logo_grob(variant) accepts "light"/"dark" to select the
  white logo for dark-background compositions
- DESCRIPTION: adds showtext, sysfonts; pins ggplot2 >= 4.0.0
- tests/testthat/test_theme.R: full test suite for colors, palettes,
  scales, both theme variants, and logo grob variants
2026-05-17 10:36:59 -06:00
jared 2d70d9aecf fix: clean up theme.R and logo.R before restyling
R-CMD-check / R CMD check (push) Successful in 1m45s
theme.R:
- Remove deprecated legend.text.align and legend.title.align (ggplot2 3.5.0)
- Remove stale @importFrom graphics plot (unused in theme function)

logo.R:
- Fix make_logo_grob() spurious two-row data frame (drop aes/data from canvas)
- Fix add_logo_ga() caption measurement always using first plot; now uses max
  across all plots with captions
- Fix add_logo_ga() ncol hardcoded to 2; now uses length(plot_list)
- Fix add_logo_ga() widths silently ignored in nrow > 1 path; now warns
- Replace deprecated qplot() in examples with ggplot() equivalents
- Fix native = T -> TRUE in plot_jpeg()
2026-05-17 10:01:35 -06:00
jared bf6f7cc480 fix: use globalVariables for state.abb/state.name, regenerate docs
R-CMD-check / R CMD check (push) Successful in 1m36s
@importFrom does not work for base R lazy data objects — replaced with
utils::globalVariables() in the package file to suppress the R CMD check
NOTE. Removed the invalid @importFrom datasets tag from utils.R.

Also commits all man/ and NAMESPACE changes from devtools::document().
2026-05-17 09:36:59 -06:00
jared b84fc2f7d0 fix: resolve all R CMD check errors and notes
R-CMD-check / R CMD check (push) Failing after 1m32s
- Wrap get_fips/get_stabbr examples in dontrun (tidycensus is Suggests)
- Add .gitea to .Rbuildignore (hidden dir note)
- Add importFrom(datasets, state.abb/state.name) for postcode_lookup
- Remove LazyData: true (no data/ directory)
2026-05-17 09:30:01 -06:00
jared d17a4fbc7c ci: use rocker container with shell git checkout
R-CMD-check / R CMD check (push) Failing after 2m13s
Replace actions/checkout@v4 (JS action, requires Node.js) with plain git
commands using the auto-injected gitea.token for auth. This lets the workflow
run entirely inside rocker/r-ver:4.4 without any Node.js dependency.
2026-05-17 09:15:24 -06:00
jared 53299c7e88 ci: run on host runner, install R from CRAN PPA
R-CMD-check / R CMD check (push) Failing after 10m49s
Remove container directive — rocker/r-ver has no Node.js so actions/checkout
(a JS action) fails with exit 127. Install R 4.x from the official CRAN
Ubuntu PPA on the host runner instead.
2026-05-17 09:12:37 -06:00
jared 38cdaa8ea7 ci: replace Jenkins with Gitea Actions, modernize DESCRIPTION
R-CMD-check / R CMD check (push) Failing after 3s
- Add real R CMD check workflow via Gitea Actions (rocker/r-ver:4.4)
- Remove Jenkinsfile and demo workflow
- Modernize DESCRIPTION: Authors@R, R >= 4.1.0, URL/BugReports fields,
  move tidycensus from Imports to Suggests, testthat edition 3
- Fix agresti_coull_interval: correct implementation, export it
- Convert get_fips/get_stabbr to use requireNamespace for tidycensus
- Remove civilytics:: self-reference in rnh()
2026-05-17 09:05:45 -06:00
Jared Knowles 01c9d1c796 test actions
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 36s
Gitea Organization/civilyticsR/pipeline/head This commit looks good
2025-04-04 11:51:19 -04:00
jared b59cbeb9b6 Merge pull request 'fix proportion ci calc and theme' (#2) from feature-prop_int into master
Gitea Organization/civilyticsR/pipeline/head This commit looks good
Reviewed-on: #2
2024-09-19 16:17:32 -04:00
104 changed files with 7891 additions and 1112 deletions
+5 -1
View File
@@ -1,8 +1,12 @@
^.*\.Rproj$
^\.Rproj\.user$
^\.github$
^\.gitea$
^README\.Rmd$
^Makefile$
^Jenkinsfile$
^Dockerfile$
^LICENSE\.md$
^\.claude$
^\.playwright-mcp$
^civilytics-site\.png$
^Rplots\.pdf$
+53
View File
@@ -0,0 +1,53 @@
name: R-CMD-check
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
check:
name: R CMD check
runs-on: ubuntu-latest
container: rocker/r-ver:4.6
steps:
# Plain git checkout — no Node.js required, works in any container.
# gitea.token is automatically injected by the runner for auth.
- name: Check out code
env:
REPO_TOKEN: ${{ gitea.token }}
run: |
apt-get update -qq && apt-get install -y --no-install-recommends git
git config --global --add safe.directory "$PWD"
git init
git remote add origin \
"https://oauth2:${REPO_TOKEN}@gitea.civilytics.org/${{ gitea.repository }}.git"
git fetch --depth=1 origin "${{ gitea.sha }}"
git checkout FETCH_HEAD
- name: Install system dependencies
run: |
apt-get install -y --no-install-recommends \
libcurl4-openssl-dev \
libssl-dev \
libxml2-dev \
libcairo2-dev \
libjpeg-dev \
libpng-dev \
libfontconfig1-dev \
libharfbuzz-dev \
libfribidi-dev \
libfreetype6-dev \
libtiff5-dev
- name: Install R dependencies
run: |
install.packages(c("remotes", "rcmdcheck"))
remotes::install_deps(dependencies = TRUE)
shell: Rscript {0}
- name: Run R CMD check
run: rcmdcheck::rcmdcheck(args = "--no-manual", error_on = "warning")
shell: Rscript {0}
+28 -14
View File
@@ -1,26 +1,40 @@
Package: civilytics
Type: Package
Title: Utilities Functions for Civilytics
Version: 0.2.0
Author: Jared E. Knowles <jared@civilytics.com>
Maintainer: Jared E. Knowles <jared@civilytics.com>
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.
Title: Brand Themes, Color Palettes, and Utility Functions for Civilytics
Version: 0.3.1
Authors@R:
person("Jared", "E. Knowles", email = "jared@civilytics.com",
role = c("aut", "cre"))
Description: Provides a complete ggplot2 brand theme system for Civilytics
Consulting LLC, including editorial light, dark, and slide-optimized themes;
10 curated color palettes for qualitative, sequential, and diverging data;
logo composition utilities; Quarto themes for HTML reports, PDF, Typst, and
Reveal.js presentations; and data-wrangling helpers for public-sector
analysis.
License: LGPL (>= 3)
URL: https://gitea.civilytics.org/Civilytics/civilyticsR
BugReports: https://gitea.civilytics.org/Civilytics/civilyticsR/issues
Depends:
R (>= 2.15.1)
R (>= 4.1.0)
Imports:
ggplot2,
ggplot2 (>= 4.0.0),
jpeg,
jsonlite,
png,
stringr,
gridExtra,
tidycensus,
grid,
stringdist
stringdist,
showtext,
sysfonts
Encoding: UTF-8
LazyData: true
Suggests:
testthat
RoxygenNote: 7.3.2
testthat (>= 3.0.0),
tidycensus,
quarto,
flextable,
officer,
ragg
Config/testthat/edition: 3
Config/roxygen2/version: 8.0.0
RoxygenNote: 7.3.3
Vendored
-60
View File
@@ -1,60 +0,0 @@
pipeline {
agent {
dockerfile true
}
stages {
stage('Docker setup') {
steps {
sh '''
R --version
java --version
'''
}
}
stage('Build and test') {
stages {
stage("Build package") {
steps {
sh '''
R CMD build .
'''
}
}
stage('Check') {
steps {
sh '''
R CMD check --no-manual civilytics_0.2.0.tar.gz
'''
sh '''
R CMD INSTALL civilytics_0.2.0.tar.gz
'''
}
}
stage('testthat'){
steps {
sh '''
R -e 'testthat::test_local(".")'
'''
}
}
stage('test coverage') {
steps {
sh '''
R -e 'covr::package_coverage(".")'
'''
}
}
stage('Clean') {
steps {
sh '''
rm -rf civilytics_0.2.0.tar.gz civilytics.Rcheck
'''
}
}
}
}
}
}
+29 -2
View File
@@ -1,7 +1,15 @@
# Generated by roxygen2: do not edit by hand
export(beep)
export(add_logo)
export(add_logo_ga)
export(agresti_coull_interval)
export(civilytics_colors)
export(civilytics_load_fonts)
export(civilytics_logo)
export(civilytics_pal)
export(civilytics_palette)
export(civilytics_palettes)
export(clopper_pearson)
export(countCleanr)
export(countDots)
@@ -31,19 +39,37 @@ export(rnh)
export(round_to_nearest_half)
export(safe_max)
export(safe_ratio)
export(save_branded_flextable_png)
export(scale_color_civilytics)
export(scale_fill_civilytics)
export(simpleCap)
export(stamp_logo_png)
export(star_subs)
export(style_flextable_civilytics)
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(use_civilytics_brand)
export(use_civilytics_revealjs)
export(use_civilytics_theme)
export(waldInterval)
export(z_gap_test)
export(z_univariate)
import(ggplot2)
import(tidycensus)
importFrom(ggplot2,annotation_custom)
importFrom(ggplot2,ggplot)
importFrom(ggplot2,theme)
importFrom(graphics,plot)
importFrom(ggplot2,theme_void)
importFrom(grDevices,dev.off)
importFrom(grDevices,png)
importFrom(graphics,rasterImage)
importFrom(grid,grid.draw)
importFrom(grid,grid.newpage)
importFrom(grid,grid.raster)
importFrom(grid,rasterGrob)
importFrom(gridExtra,arrangeGrob)
importFrom(jpeg,readJPEG)
@@ -53,3 +79,4 @@ importFrom(stats,qnorm)
importFrom(stats,runif)
importFrom(stringdist,stringsim)
importFrom(stringr,str_count)
importFrom(utils,flush.console)
+31 -1
View File
@@ -1,3 +1,31 @@
#' @description
#' 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
#' }
#'
#' @section Quarto templates:
#' \itemize{
#' \item [use_civilytics_revealjs()] -- install Reveal.js slide extension
#' \item [use_civilytics_theme()] -- install HTML/PDF/Typst document theme
#' \item [use_civilytics_brand()] -- install brand.yml only (Quarto 1.5+)
#' }
#'
#' @keywords internal
#' @importFrom stats qbeta
#' @importFrom stats qnorm
@@ -7,4 +35,6 @@
## 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"))
+295
View File
@@ -0,0 +1,295 @@
#' Civilytics brand colors
#'
#' A named character vector of all Civilytics brand colors, matching the CSS
#' 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.
#' @export
#'
#' @examples
#' civilytics_colors["ink"]
#' civilytics_colors[c("navy_600", "ember_600", "teal_600")]
civilytics_colors <- c(
# Neutrals — warm paper -> civic ink
paper = "#FAF7F2",
paper_2 = "#F2EDE4",
paper_3 = "#E6DFD1",
rule = "#D6CEBD",
rule_strong = "#B8AE97",
ink = "#0E1A2B",
ink_2 = "#2B3A52",
ink_3 = "#5A6A82",
ink_4 = "#8C97AB",
# Navy — civic authority (primary brand blue)
navy_900 = "#0E1A2B", navy_800 = "#132339", navy_700 = "#1A2E4A",
navy_600 = "#22406A", navy_500 = "#2E5590", navy_400 = "#4A74B0",
navy_300 = "#7A9BCA", navy_200 = "#B3C6E0", navy_100 = "#DDE6F2",
navy_50 = "#EEF3FA",
# Ember — warm orange accent
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"
)
#' Civilytics visualization palettes
#'
#' 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.
#'
#' @format A named list of character vectors of hex color codes.
#'
#' @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)}
#' }
#'
#' @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"
)
)
#' Get colors from a Civilytics palette
#'
#' 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()].
#'
#' @param name Character. Palette name. See `names(civilytics_palettes)`.
#' @param n Integer or `NULL`. Number of colors to return. If `NULL`, returns
#' all defined stops.
#' @param reverse Logical. Reverse the palette order. Default `FALSE`.
#'
#' @return A character vector of hex color codes.
#' @export
#'
#' @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)
civilytics_palette <- function(name = "qual", n = NULL, reverse = FALSE) {
pal <- civilytics_palettes[[name]]
if (is.null(pal)) {
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
#'
#' Applies a Civilytics brand palette to the `colour` aesthetic.
#'
#' @param palette Character. Palette name. Defaults to `"qual"`.
#' @param discrete Logical. `TRUE` (default) for categorical data; `FALSE`
#' for a continuous gradient via [ggplot2::scale_color_gradientn()].
#' @param reverse Logical. Reverse the palette order. Default `FALSE`.
#' @param ... Additional arguments passed to the ggplot2 scale function.
#'
#' @return A ggplot2 scale object.
#' @export
#'
#' @examples
#' library(ggplot2)
#' ggplot(mpg, aes(displ, hwy, colour = class)) +
#' geom_point() +
#' scale_color_civilytics()
#'
#' ggplot(mpg, aes(displ, hwy, colour = cty)) +
#' geom_point() +
#' scale_color_civilytics("seq_navy", discrete = FALSE)
scale_color_civilytics <- function(palette = "qual", discrete = TRUE,
reverse = FALSE, ...) {
if (discrete) {
ggplot2::discrete_scale(
"colour",
palette = civilytics_pal(palette, reverse = reverse),
...
)
} else {
pal <- civilytics_palette(palette, reverse = reverse)
ggplot2::scale_color_gradientn(
colours = grDevices::colorRampPalette(pal)(256),
...
)
}
}
#' Civilytics fill scale for ggplot2
#'
#' Applies a Civilytics brand palette to the `fill` aesthetic.
#'
#' @param palette Character. Palette name. Defaults to `"qual"`.
#' @param discrete Logical. `TRUE` (default) for categorical data; `FALSE`
#' for a continuous gradient via [ggplot2::scale_fill_gradientn()].
#' @param reverse Logical. Reverse the palette order. Default `FALSE`.
#' @param ... Additional arguments passed to the ggplot2 scale function.
#'
#' @return A ggplot2 scale object.
#' @export
#'
#' @examples
#' library(ggplot2)
#' ggplot(mpg, aes(class, fill = class)) +
#' geom_bar() +
#' scale_fill_civilytics()
#'
#' ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
#' geom_tile() +
#' scale_fill_civilytics("seq_ember", discrete = FALSE)
scale_fill_civilytics <- function(palette = "qual", discrete = TRUE,
reverse = FALSE, ...) {
if (discrete) {
ggplot2::discrete_scale(
"fill",
palette = civilytics_pal(palette, reverse = reverse),
...
)
} else {
pal <- civilytics_palette(palette, reverse = reverse)
ggplot2::scale_fill_gradientn(
colours = grDevices::colorRampPalette(pal)(256),
...
)
}
}
+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)
}
+55
View File
@@ -0,0 +1,55 @@
# Font family name constants used as defaults in theme functions.
# These match the --cv-font-* CSS custom properties on www.civilytics.com.
CV_FONT_DISPLAY <- "Libre Franklin" # headings / display text
CV_FONT_SANS <- "Inter" # axis text, legends, UI elements
CV_FONT_SERIF <- "Source Serif 4" # body prose / editorial long-form
CV_FONT_MONO <- "JetBrains Mono" # code, data tables, numeric callouts
# Internal flag so civilytics_load_fonts() is idempotent within a session.
.cv_fonts_loaded <- FALSE
#' Load Civilytics brand fonts
#'
#' Downloads Inter and Libre Franklin from Google Fonts via
#' [sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so
#' that all graphics devices render text with those fonts. This is called
#' automatically when the package loads; use this function to retry if the
#' initial load failed (e.g., the machine was offline at load time).
#'
#' @return Invisibly returns `NULL`.
#' @export
#'
#' @examples
#' \dontrun{
#' civilytics_load_fonts()
#' }
civilytics_load_fonts <- function() {
sysfonts::font_add_google("Inter", family = "Inter")
sysfonts::font_add_google("Libre Franklin", family = "Libre Franklin")
sysfonts::font_add_google("Source Serif 4", family = "Source Serif 4")
sysfonts::font_add_google("JetBrains Mono", family = "JetBrains Mono")
showtext::showtext_auto()
.cv_fonts_loaded <<- TRUE
invisible(NULL)
}
.onLoad <- function(libname, pkgname) {
tryCatch(
civilytics_load_fonts(),
error = function(e) {
# Store the failure so .onAttach can surface a message to the user.
options(.civilytics_fonts_failed = TRUE)
}
)
}
.onAttach <- function(libname, pkgname) {
if (isTRUE(getOption(".civilytics_fonts_failed"))) {
packageStartupMessage(
"[civilytics] Brand fonts (Inter, Libre Franklin, Source Serif 4, ",
"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."
)
}
}
+317 -38
View File
@@ -9,11 +9,13 @@
#' @importFrom jpeg readJPEG
#' @importFrom graphics rasterImage
#' @examples
#' img <- system.file("img","civilytics_logo.jpg",package="civilytics")
#' \dontrun{
#' img <- system.file("img","Knowles_Headshot_2019_good.jpg",package="civilytics")
#' plot_jpeg(img)
#' }
plot_jpeg <- function(path, add=FALSE, upscale = TRUE)
{
jpg = readJPEG(path, native = T) # read the file
jpg = readJPEG(path, native = TRUE) # read the file
res = dim(jpg)[2:1] # get the resolution, [x, y]
if (upscale){
res <- res * 3
@@ -44,29 +46,73 @@ get_png <- function(filename) {
#' @param plot a ggplot2 grob
#' @param logo a logo grob created by make_logo_grob()
#' @param margin_param a numeric specifying what margin to add or subtract to align the logo
#' @param font_scale Numeric. Multiplicative scaling factor applied to text
#' sizes before composing the plot with the logo. Default `1.1` inflates
#' text by ~10 \% to compensate for the viewport shrinkage caused by
#' [gridExtra::arrangeGrob()]. Set to `1` to disable.
#' @param position Character. Corner placement for the logo: `"bottom-right"`
#' (default), `"bottom-left"`, `"top-right"`, or `"top-left"`. Controls
#' whether the logo is placed above or below the plot.
#'
#' @return a grob with a logo attached to it ready to plot
#' @importFrom ggplot2 theme
#' @importFrom gridExtra arrangeGrob
#' @export
add_logo <- function(plot, logo, margin_param = NULL) {
if(has_caption(plot)) {
# convert the caption size to a negative number and on the "pt" scale
# p1$theme$plot.caption$size * 1.1
# Count the number of lines, which is this + 1
# For each line, we can add a certain negative space to align the logo
cap_lines <- measure_caption(plot)
if (!is.null(margin_param)) {
plot <- plot + theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt"))
} else {
plot <- plot + theme(plot.margin = unit(c(7, 7, cap_lines * -52, 7), "pt"))
}
} else {
plot <- plot + theme(plot.margin = unit(c(7, 7, -14, 7), "pt"))
add_logo <- function(plot, logo, margin_param = NULL, font_scale = 1.1,
position = c("bottom-right", "bottom-left",
"top-right", "top-left")) {
position <- match.arg(position)
at_top <- grepl("top", position)
# Capture the plot's background color so we can fill the entire composed
# grob with it. The logo grob must stay transparent (theme_void) so that
# caption/axis text overlapping via negative margins remains visible.
bg_fill <- plot$theme$plot.background$fill
# Inflate text sizes to compensate for arrangeGrob viewport shrinkage.
# All theme text elements use rel() sizing, so scaling the root 'text'
# element cascades to titles, axis labels, legends, captions, and strips.
if (!is.null(font_scale) && font_scale != 1) {
base_size <- plot$theme$text$size %||% 14
plot <- plot + ggplot2::theme(
text = ggplot2::element_text(size = base_size * font_scale)
)
}
arrangeGrob(plot, logo, heights = c(0.93, 0.1),
padding = unit(0.1, "line"))
if (at_top) {
# For top placement, tighten the top margin of the plot
if (!is.null(margin_param)) {
plot <- plot + theme(plot.margin = unit(c(margin_param, 7, 7, 7), "pt"))
} else {
plot <- plot + theme(plot.margin = unit(c(-14, 7, 7, 7), "pt"))
}
composed <- arrangeGrob(logo, plot, heights = c(0.1, 0.93),
padding = unit(0.1, "line"))
} else {
# Bottom placement (original behaviour)
if(has_caption(plot)) {
cap_lines <- measure_caption(plot)
if (!is.null(margin_param)) {
plot <- plot + theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt"))
} else {
plot <- plot + theme(plot.margin = unit(c(7, 7, cap_lines * -52, 7), "pt"))
}
} else {
plot <- plot + theme(plot.margin = unit(c(7, 7, -14, 7), "pt"))
}
composed <- arrangeGrob(plot, logo, heights = c(0.93, 0.1),
padding = unit(0.1, "line"))
}
# Wrap with a full-bleed background rect so any transparent areas (the
# logo strip, padding gaps) pick up the plot's background color instead
# of the device default (white).
if (!is.null(bg_fill) && !is.na(bg_fill)) {
bg_rect <- grid::rectGrob(gp = grid::gpar(fill = bg_fill, col = NA))
grid::grobTree(bg_rect, composed)
} else {
composed
}
}
#' Measure a ggplot2 object caption
@@ -79,8 +125,8 @@ add_logo <- function(plot, logo, margin_param = NULL) {
#' @export
#'
#' @examples
#' p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
#' measure_caption(p1) # Should equal 1 since no caption is required
#' p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
#' measure_caption(p1) # Should equal 1 since no caption is present
measure_caption <- function(gg) {
if (has_caption(gg)) {
stringr::str_count(gg$labels$caption, pattern = "\n") + 1
@@ -99,19 +145,21 @@ measure_caption <- function(gg) {
#' @export
#'
#' @examples
#' p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
#' p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
#' has_caption(p1) # FALSE
has_caption <- function(gg) {
any(names(gg$labels) == "caption")
}
#' Add a logo to a ggplot2 object
#' Add a logo to multiple ggplot2 objects
#'
#' @param plot_list a list containing ggplot2 objects
#' @param logo a grob containing the logo created with `make_logo_grob`
#' @param nrow an integer, default = 1, for the number of rows to align the plots in
#' @param widths an optional vector the same length as plot_list with the widths for each plot
#' @param margin_param a number giving the adjustment up or down to help manually align logo and captions
#' @param font_scale Numeric. Multiplicative scaling factor applied to text
#' sizes before composing. Default `1.1`. See [add_logo()] for details.
#'
#' @return a grid object
#' @note The resulting object needs to be drawn to the screen using grid.draw()
@@ -121,18 +169,34 @@ has_caption <- function(gg) {
#' @export
#'
#' @examples
#' \dontrun{
#' library(ggplot2); library(grid)
#' tmp_plot <- ggplot(mtcars) + aes(x = hp, y = disp) + geom_point() + theme_civilytics()
#' tmp_logo <- make_logo_grob()
#' plot_and_logo <- add_logo(tmp_plot, tmp_logo)
#' grid.draw(plot_and_logo)
#' dev.off()
add_logo_ga <- function(plot_list, logo, nrow = 1, widths = NULL, margin_param = NULL) {
#' }
add_logo_ga <- function(plot_list, logo, nrow = 1, widths = NULL,
margin_param = NULL, font_scale = 1.1) {
# Capture the background color from the first plot
bg_fill <- plot_list[[1]]$theme$plot.background$fill
# Inflate text sizes to compensate for viewport shrinkage
if (!is.null(font_scale) && font_scale != 1) {
plot_list <- lapply(plot_list, function(p) {
base_size <- p$theme$text$size %||% 14
p + ggplot2::theme(
text = ggplot2::element_text(size = base_size * font_scale)
)
})
}
# Change position of logo depending on if plot has a caption
if (!is.null(margin_param)) {
margin <- theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt"))
} else if (any(unlist(lapply(plot_list, has_caption)))) {
cap_lines <- measure_caption(plot_list[[1]]) # measure caption in first plot
cap_lines <- max(sapply(plot_list, measure_caption))
margin <- theme(plot.margin = unit(c(7, 7, -7 * sqrt(cap_lines), 7), "pt"))
} else {
margin <- theme(plot.margin = unit(c(7, 7, 7, 7), "pt"))
@@ -145,29 +209,244 @@ add_logo_ga <- function(plot_list, logo, nrow = 1, widths = NULL, margin_param =
}
if (nrow != 1) {
hold <- arrangeGrob(grobs = plot_list, nrow = nrow, ncol = 1, widths = widths)
if (!is.null(widths)) warning("`widths` is ignored when `nrow > 1`")
hold <- arrangeGrob(grobs = plot_list, nrow = nrow, ncol = 1)
} else {
hold <- arrangeGrob(grobs = plot_list, nrow = 1, ncol = 2, widths = widths)
hold <- arrangeGrob(grobs = plot_list, nrow = 1, ncol = length(plot_list), widths = widths)
}
arrangeGrob(hold, logo, heights = c(0.93, .07))
composed <- arrangeGrob(hold, logo, heights = c(0.93, .07))
if (!is.null(bg_fill) && !is.na(bg_fill)) {
bg_rect <- grid::rectGrob(gp = grid::gpar(fill = bg_fill, col = NA))
grid::grobTree(bg_rect, composed)
} else {
composed
}
}
#' Get a Civilytics Logo grob
#'
#' @return a gg object which contains the logo file stored as a Grob suitable for manipulating in
#' grid
#' Returns a ggplot object containing the Civilytics logo as a rasterGrob,
#' ready to compose with plots via [add_logo()], [add_logo_ga()], or the
#' pipe-friendly [civilytics_logo()].
#'
#' @param type Character. `"wordmark"` (default) uses the full wordmark.
#' `"mark"` uses the compact C-pulse icon only.
#' @param variant Character. `"light"` (default) uses the dark logo for light
#' backgrounds. `"dark"` uses the reverse (light) logo for dark backgrounds
#' (pairs with [theme_civilytics_dark()]).
#' @param position Character. Corner placement for the logo: `"bottom-right"`
#' (default), `"bottom-left"`, `"top-right"`, or `"top-left"`. Controls
#' horizontal alignment of the logo grob.
#'
#' @return A ggplot object (class `"gg"`) containing the logo grob.
#' @export
#' @import ggplot2
#' @importFrom ggplot2 ggplot theme_void annotation_custom
#' @examples
#' logo <- make_logo_grob()
#' class(logo) # gg
make_logo_grob <- function() {
logo_grob <- ggplot(mapping = aes(x = 0:1, y = 1)) +
theme_void() +
annotation_custom(get_png(system.file("img", "civilytics_logo.png",
package="civilytics")), xmin= 0.7, xmax = 1)
logo_grob
#' logo <- make_logo_grob() # wordmark, light
#' logo <- make_logo_grob("mark", "dark") # mark, dark
#' logo <- make_logo_grob(position = "bottom-left") # left-aligned
#' class(logo) # "gg" "ggplot"
make_logo_grob <- function(type = c("wordmark", "mark"),
variant = c("light", "dark"),
position = c("bottom-right", "bottom-left",
"top-right", "top-left")) {
type <- match.arg(type)
variant <- match.arg(variant)
position <- match.arg(position)
img_file <- switch(
paste(type, variant, sep = "_"),
wordmark_light = "civilytics-wordmark.png",
wordmark_dark = "civilytics-wordmark-reverse.png",
mark_light = "civilytics-mark.png",
mark_dark = "civilytics-mark-reverse.png"
)
align_left <- grepl("left", position)
if (align_left) {
xmin <- 0
xmax <- if (type == "mark") 0.07 else 0.35
} else {
xmin <- if (type == "mark") 0.93 else 0.65
xmax <- 1
}
ggplot2::ggplot() +
ggplot2::theme_void() +
ggplot2::annotation_custom(
get_png(system.file("img", img_file, package = "civilytics")),
xmin = xmin, xmax = xmax
)
}
#' Add a Civilytics logo to a ggplot (pipe-friendly)
#'
#' A convenience wrapper that creates the logo grob and attaches it to a
#' corner of the plot in one call. Designed for use with the base pipe `|>`.
#'
#' **Important:** R's `|>` has *higher* precedence than `+`, so you must
#' wrap the ggplot chain in parentheses before piping:
#'
#' ```
#' (ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics()) |>
#' civilytics_logo()
#' ```
#'
#' @param plot A ggplot object.
#' @param type Character. `"wordmark"` (default) or `"mark"`. Passed to
#' [make_logo_grob()].
#' @param variant Character. `"light"` (default) or `"dark"`. Passed to
#' [make_logo_grob()].
#' @param position Character. Corner placement for the logo: `"bottom-right"`
#' (default), `"bottom-left"`, `"top-right"`, or `"top-left"`.
#' @param margin_param Numeric or `NULL`. Manual margin adjustment passed to
#' [add_logo()].
#' @param font_scale Numeric. Inflate text sizes by this factor to compensate
#' for viewport shrinkage when composing with [gridExtra::arrangeGrob()].
#' Default `1.1` (~10 \% inflation). Set to `1` to disable.
#'
#' @return A grob (from [gridExtra::arrangeGrob()]) ready to draw with
#' [grid::grid.draw()].
#' @export
#'
#' @examples
#' \dontrun{
#' library(ggplot2); library(grid)
#'
#' # Pipe usage — parentheses required around the ggplot chain
#' (ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics()) |>
#' civilytics_logo() |>
#' grid.draw()
#'
#' # Top-right placement
#' (ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics()) |>
#' civilytics_logo(position = "top-right") |>
#' grid.draw()
#'
#' # Bottom-left with mark
#' (ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics()) |>
#' civilytics_logo(type = "mark", position = "bottom-left") |>
#' grid.draw()
#'
#' # Dark theme with mark in top-left
#' (ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics_dark()) |>
#' civilytics_logo(variant = "dark", type = "mark",
#' position = "top-left") |>
#' grid.draw()
#' }
civilytics_logo <- function(plot,
type = c("wordmark", "mark"),
variant = c("light", "dark"),
position = c("bottom-right", "bottom-left",
"top-right", "top-left"),
margin_param = NULL,
font_scale = 1.1) {
position <- match.arg(position)
logo <- make_logo_grob(type = type, variant = variant, position = position)
add_logo(plot, logo, margin_param = margin_param, font_scale = font_scale,
position = position)
}
#' Stamp the Civilytics logo onto a saved raster (PNG) image
#'
#' The raster analogue of [civilytics_logo()] for outputs that are already
#' rendered to a file rather than held as a ggplot/grob — e.g. a `flextable`
#' exported to PNG, or any `grDevices::png()` / `ragg::agg_png()` output.
#' Resolves the *same* brand asset that [make_logo_grob()] uses, so file-based
#' tables stay visually consistent with logo-branded plots, and composites it
#' into a corner of the image. Pure base-graphics + grid + png — no new
#' package dependencies.
#'
#' @param path Character. Path to the PNG to stamp. The file is overwritten
#' in place at its original pixel dimensions.
#' @param type Character. `"wordmark"` (default) or `"mark"`. As in
#' [make_logo_grob()].
#' @param variant Character. `"light"` (default, dark logo for light
#' backgrounds) or `"dark"` (reverse logo for dark backgrounds).
#' @param position Character. Corner placement: `"bottom-right"` (default),
#' `"bottom-left"`, `"top-right"`, or `"top-left"`.
#' @param width_frac Numeric. Logo width as a fraction of the image width
#' (default `0.15`). Height follows from the logo's aspect ratio.
#' @param margin_frac Numeric. Padding from the edges as a fraction of the
#' image width (default `0.02`).
#'
#' @return `path`, invisibly.
#' @export
#' @importFrom png readPNG
#' @importFrom grid grid.newpage grid.raster
#' @importFrom grDevices png dev.off
#' @examples
#' \dontrun{
#' # Brand a table exported to PNG so it matches civilytics_logo()-branded plots
#' ragg::agg_png("table.png", width = 8, height = 4, units = "in", res = 200)
#' plot(flextable::flextable(head(mtcars)))
#' dev.off()
#' stamp_logo_png("table.png") # wordmark, bottom-right
#' stamp_logo_png("table.png", type = "mark", position = "bottom-left")
#' }
stamp_logo_png <- function(path,
type = c("wordmark", "mark"),
variant = c("light", "dark"),
position = c("bottom-right", "bottom-left",
"top-right", "top-left"),
width_frac = 0.15,
margin_frac = 0.02) {
type <- match.arg(type)
variant <- match.arg(variant)
position <- match.arg(position)
stopifnot(file.exists(path))
# Same asset selection as make_logo_grob() so files match branded plots.
img_file <- switch(
paste(type, variant, sep = "_"),
wordmark_light = "civilytics-wordmark.png",
wordmark_dark = "civilytics-wordmark-reverse.png",
mark_light = "civilytics-mark.png",
mark_dark = "civilytics-mark-reverse.png"
)
logo_path <- system.file("img", img_file, package = "civilytics")
if (!nzchar(logo_path)) {
stop("Civilytics logo asset not found in the 'civilytics' package: ", img_file)
}
base_img <- png::readPNG(path) # height x width x channels, values in [0, 1]
logo_img <- png::readPNG(logo_path)
h <- dim(base_img)[1]
w <- dim(base_img)[2]
aspect <- dim(logo_img)[1] / dim(logo_img)[2] # logo height / width
# Sizes/margins are expressed relative to image WIDTH, then converted to the
# device's npc units (which scale with the viewport's own width and height).
lw <- width_frac
lh <- width_frac * aspect * (w / h)
mx <- margin_frac
my <- margin_frac * (w / h)
x <- if (grepl("right", position)) 1 - mx else mx
y <- if (grepl("top", position)) 1 - my else my
just <- c(if (grepl("right", position)) "right" else "left",
if (grepl("top", position)) "top" else "bottom")
grDevices::png(path, width = w, height = h, units = "px")
on.exit(grDevices::dev.off(), add = TRUE)
grid::grid.newpage()
grid::grid.raster(base_img, width = 1, height = 1, interpolate = FALSE)
grid::grid.raster(logo_img, x = x, y = y, width = lw, height = lh,
just = just, interpolate = TRUE)
invisible(path)
}
+153
View File
@@ -0,0 +1,153 @@
#' Send a CLI beep / desktop notification
#'
#' Plays an audible beep in the terminal and/or sends a desktop notification
#' when a long-running R script completes or reaches a milestone.
#'
#' @param msg Character. Optional message to include in the notification.
#' When `type = "notify"`, this becomes the notification body.
#' @param type Character. Notification method:
#' - `"beep"` (default): emit a terminal bell character (`\007`).
#' Works in any terminal that supports the bell.
#' - `"notify"`: send a desktop notification via `notify-send` (Linux) or
#' `osascript` (macOS). Falls back to `"beep"` if neither tool is found.
#' - `"webhook"`: POST a JSON payload to a URL. Requires `url` argument.
#' - `"all"`: play beep + send desktop notification (webhook only if `url`
#' is provided).
#' @param url Character. Webhook URL for `type = "webhook"` or `"all"`.
#' A JSON payload is POSTed with keys `message`, `status`, and `timestamp`.
#' @param status Character. Status label for the notification (default `"done"`).
#' Used in the notification title and webhook payload.
#' @param timeout Numeric. Seconds to wait for the webhook POST to complete
#' (default `5`). Ignored for non-webhook types.
#' @param quiet Logical. If `TRUE`, suppress the terminal beep even when
#' `type` includes `"beep"`. Useful for silent background runs.
#'
#' @return Invisible `NULL`.
#'
#' @section Requirements:
#' - `type = "notify"` requires `notify-send` (Linux) or `osascript` (macOS).
#' - `type = "webhook"` requires network access to the provided URL.
#'
#' @section Examples:
#' \preformatted{
#' # Simple terminal beep
#' beep()
#'
#' # Desktop notification with message
#' beep("Analysis complete!", type = "notify")
#'
#' # Send to a webhook (e.g., Slack, Discord, custom endpoint)
#' beep("Job finished", type = "webhook",
#' url = "https://hooks.slack.com/services/...")
#'
#' # Beep + desktop notification
#' beep("Processing done", type = "all")
#' }
#'
#' @export
#'
beep <- function(msg = "done",
type = c("beep", "notify", "webhook", "all"),
url = NULL,
status = "done",
timeout = 5,
quiet = FALSE) {
type <- match.arg(type)
# -- Terminal beep ----------------------------------------------------------
if (!quiet && grepl("beep", type)) {
cat("\007")
flush.console()
}
# -- Desktop notification ---------------------------------------------------
if (grepl("notify", type)) {
.send_desktop_notify(msg, status)
}
# -- Webhook ----------------------------------------------------------------
if (grepl("webhook", type) && !is.null(url)) {
.send_webhook(url, msg, status, timeout)
}
invisible(NULL)
}
# -- Internal helpers ---------------------------------------------------------
#' Send a desktop notification via notify-send or osascript
#'
#' @param msg Message body
#' @param status Status label for the title
#' @keywords internal
.send_desktop_notify <- function(msg, status) {
# Linux: notify-send
if (.has_command("notify-send")) {
system2("notify-send", args = c(status, msg),
stdout = TRUE, stderr = TRUE)
return(invisible(NULL))
}
# macOS: osascript
if (.has_command("osascript")) {
system2("osascript",
args = c("-e",
paste0("display notification \"", msg,
"\" with title \"", status, "\"")),
stdout = TRUE, stderr = TRUE)
return(invisible(NULL))
}
# Neither tool available — silently skip
invisible(NULL)
}
#' POST a JSON payload to a webhook URL
#'
#' @param url Webhook URL
#' @param msg Message body
#' @param status Status label
#' @param timeout Seconds to wait for the request
#' @keywords internal
.send_webhook <- function(url, msg, status, timeout) {
payload <- jsonlite::toJSON(list(
message = msg,
status = status,
timestamp = format(Sys.time(), "%Y-%m-%dT%H:%M:%S%z")
), auto_unbox = TRUE)
# Use curl via system2() for maximum compatibility (no extra R deps).
# system2() passes arguments directly to the executable without shell
# interpolation, avoiding command injection.
if (.has_command("curl")) {
system2("curl",
args = c("-s", "-X", "POST",
"-H", "Content-Type: application/json",
"-d", payload,
url,
"--max-time", as.character(timeout)),
stdout = TRUE, stderr = TRUE)
} else if (.has_command("wget")) {
system2("wget",
args = c("-q", "-O", "/dev/null",
paste0("--post-data=", payload),
paste0("--header=Content-Type: application/json"),
paste0("--timeout=", timeout),
url),
stdout = TRUE, stderr = TRUE)
}
invisible(NULL)
}
#' Check if a command exists on the system PATH
#'
#' @param cmd Command name
#' @return Logical
#' @keywords internal
.has_command <- function(cmd) {
Sys.which(cmd) != ""
}
+16 -9
View File
@@ -72,14 +72,21 @@ waldInterval <- function(x, n, conf.level = 0.95){
#' Calculate the Agresti-Coull interval
#'
#' @param num a number of successes
#' @param den a number of trials
#' @param conf.level default 0.95, set the confidence interval to return
#' @param num number of successes
#' @param den number of trials
#' @param conf.level default 0.95, confidence level for the interval
#'
#' @return an interval
agresti_coull_interval <- function(num, den, conf.level) {
num <- num + 2
den <- den + 4
return(num/den)
#' @return three values forming the lower bound, observed proportion, and upper bound
#' @export
#'
#' @examples
#' agresti_coull_interval(20, 40)
#' agresti_coull_interval(2, 100, conf.level = 0.99)
agresti_coull_interval <- function(num, den, conf.level = 0.95) {
z <- qnorm(1 - (1 - conf.level) / 2)
n_tilde <- den + z^2
p_tilde <- (num + z^2 / 2) / n_tilde
margin <- z * sqrt(p_tilde * (1 - p_tilde) / n_tilde)
obs <- num / den
return(c("low" = p_tilde - margin, "observed" = obs, "high" = p_tilde + margin))
}
+195
View File
@@ -0,0 +1,195 @@
#' Install Civilytics Quarto Themes and Templates
#'
#' Helper functions that copy Civilytics Quarto assets from the installed
#' package into your Quarto project directory. Logos are stored in a single
#' canonical location (`inst/img/`) and copied to the paths expected by each
#' template at install time.
#'
#' @name quarto-helpers
NULL
# -- internal utilities -------------------------------------------------------
#' Copy a package file to a project directory
#'
#' @param src Relative path within the installed package (under `inst/`).
#' @param dst Destination path relative to `path`.
#' @param path Project root.
#' @param force Overwrite existing files?
#' @return Invisible logical indicating success.
#' @keywords internal
.copy_pkg_file <- function(src, dst, path, force) {
from <- system.file(src, package = "civilytics", mustWork = TRUE)
to <- file.path(path, dst)
dir.create(dirname(to), recursive = TRUE, showWarnings = FALSE)
if (file.exists(to) && !force) {
message(" skip: ", dst, " (already exists; use force = TRUE to overwrite)")
return(invisible(FALSE))
}
file.copy(from, to, overwrite = force)
message(" copy: ", dst)
invisible(TRUE)
}
#' Copy logo SVGs from inst/img/ to a target directory
#'
#' @param dest_dir Destination directory relative to `path`.
#' @param path Project root.
#' @param force Overwrite existing files?
#' @param files Character vector of logo filenames to copy.
#' @keywords internal
.copy_logos <- function(dest_dir, path, force,
files = c("civilytics-mark.svg",
"civilytics-mark-reverse.svg",
"civilytics-wordmark.svg",
"civilytics-wordmark-reverse.svg",
"civilytics-pulse.svg")) {
for (f in files) {
.copy_pkg_file(file.path("img", f), file.path(dest_dir, f), path, force)
}
}
# -- public API ----------------------------------------------------------------
#' Install the Civilytics Reveal.js extension
#'
#' Copies the Civilytics Reveal.js Quarto extension into
#' `_extensions/civilytics-reveal/` under `path`, including brand logos
#' from the package. After running this function, add
#' `format: civilytics-reveal-revealjs` to your `.qmd` YAML front-matter.
#'
#' @param path Character. Project directory to install into. Default `"."`.
#' @param force Logical. Overwrite existing files? Default `FALSE`.
#'
#' @return Invisible `NULL`.
#' @export
#'
#' @examples
#' \dontrun{
#' use_civilytics_revealjs()
#' }
use_civilytics_revealjs <- function(path = ".", force = FALSE) {
path <- normalizePath(path, mustWork = TRUE)
message("Installing Civilytics Reveal.js extension into: ", path)
ext_src <- "quarto/extensions/civilytics-reveal"
ext_dst <- "_extensions/civilytics-reveal"
ext_files <- c(
"_extension.yml",
"civilytics.scss",
"civilytics.css",
"civilytics-head.html",
"civilytics-after.html"
)
for (f in ext_files) {
.copy_pkg_file(file.path(ext_src, f), file.path(ext_dst, f), path, force)
}
# Logos — single source from inst/img/
.copy_logos(file.path(ext_dst, "assets"), path, force)
message("\nDone! Add this to your .qmd front-matter:\n")
message("---")
message("format: civilytics-reveal-revealjs")
message("---")
invisible(NULL)
}
#' Install the Civilytics document theme
#'
#' Copies Civilytics HTML, PDF (LaTeX), and Typst theme files into
#' your Quarto project. Also installs `_brand.yml` and the logo
#' assets it references.
#'
#' @inheritParams use_civilytics_revealjs
#'
#' @return Invisible `NULL`.
#' @export
#'
#' @examples
#' \dontrun{
#' use_civilytics_theme()
#' }
use_civilytics_theme <- function(path = ".", force = FALSE) {
path <- normalizePath(path, mustWork = TRUE)
message("Installing Civilytics document theme into: ", path)
# Brand file
.copy_pkg_file("quarto/_brand.yml", "_brand.yml", path, force)
# HTML theme
theme_files <- c("civilytics.scss", "_tokens.scss", "extras.css")
for (f in theme_files) {
.copy_pkg_file(file.path("quarto/theme", f), file.path("theme", f), path, force)
}
# LaTeX
latex_files <- c("civilytics.tex", "civilytics-title.tex")
for (f in latex_files) {
.copy_pkg_file(file.path("quarto/latex", f), file.path("latex", f), path, force)
}
# Typst — shipped as template-partials so Quarto keeps its Skylighting
# definitions and syntax-highlighted code blocks render (see issue #12)
typst_files <- c("typst-template.typ", "typst-show.typ")
for (f in typst_files) {
.copy_pkg_file(file.path("quarto/typst", f), file.path("typst", f), path, force)
}
# Logos — for _brand.yml (expects assets/logo/)
.copy_logos("assets/logo", path, force)
# Example report
.copy_pkg_file("quarto/examples/report.qmd", "examples/report.qmd", path, force)
message("\nDone! Example YAML for a report:\n")
message("---")
message("format:")
message(" html:")
message(" theme: theme/civilytics.scss")
message(" css: theme/extras.css")
message(" pdf:")
message(" include-in-header: latex/civilytics.tex")
message(" include-before-body: latex/civilytics-title.tex")
message(" typst:")
message(" template-partials:")
message(" - typst/typst-template.typ")
message(" - typst/typst-show.typ")
message("---")
message("\nSee examples/report.qmd for a complete example.")
invisible(NULL)
}
#' Install the Civilytics brand file only
#'
#' Copies `_brand.yml` and the logo assets it references into your
#' Quarto project. This gives you Quarto 1.5+ automatic brand
#' styling (colors, fonts, logos) without the full theme SCSS.
#'
#' @inheritParams use_civilytics_revealjs
#'
#' @return Invisible `NULL`.
#' @export
#'
#' @examples
#' \dontrun{
#' use_civilytics_brand()
#' }
use_civilytics_brand <- function(path = ".", force = FALSE) {
path <- normalizePath(path, mustWork = TRUE)
message("Installing Civilytics brand file into: ", path)
.copy_pkg_file("quarto/_brand.yml", "_brand.yml", path, force)
.copy_logos("assets/logo", path, force)
message("\nDone! Quarto 1.5+ will auto-apply brand colors, fonts, and logos.")
message("See: https://quarto.org/docs/authoring/brand.html")
invisible(NULL)
}
+619 -165
View File
@@ -1,168 +1,622 @@
#' Make the Civilytics plot theme
#' Civilytics ggplot2 theme
#'
#' @param font_size default 14, a number representing the base font for the theme
#' @param font_family default "", a character for the font family to use in the theme
#' @param line_size default 0.5, the line size to use for the theme
#' @param rel_small default 12/14, the scale factor to create a small font from the base font_size
#' @param rel_tiny default 11/14, the scale factor to create a tiny font from the base font_size
#' @param rel_large default 16/14, the scale factor to create a large font from the base font_size
#' @importFrom graphics plot
#' @return a ggplot2 theme object suitable for combining with ggplot objects to theme them
#' A complete ggplot2 theme built on [ggplot2::theme_grey()] using the
#' Civilytics brand color palette and typography. Requires ggplot2 >= 4.0.0
#' 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
#' automatically via [showtext] when the package is attached. Call
#' [civilytics_load_fonts()] to reload them if needed.
#'
#' @param font_size Numeric. Base font size in points. Default `14`.
#' @param font_family Character. Font family for body/axis text. Default
#' `"Inter"` (loaded via showtext).
#' @param title_family Character. Font family for plot titles and strip labels.
#' Default `"Libre Franklin"` (loaded via showtext).
#' @param line_size Numeric. Base line width. Default `0.5`.
#' @param rel_small Numeric. Scale factor for small text relative to
#' `font_size`. Default `12/14`.
#' @param rel_tiny Numeric. Scale factor for tiny text relative to `font_size`.
#' Default `11/14`.
#' @param rel_large Numeric. Scale factor for large text (titles) relative to
#' `font_size`. Default `20/14` (~1.43x), matching the Civilytics editorial
#' design system.
#' @param ink Character. Hex code for foreground/text color. Defaults to
#' [civilytics_colors]`["ink"]` (`#0E1A2B`).
#' @param paper Character. Hex code for background color. Defaults to
#' [civilytics_colors]`["paper"]` (`#FAF7F2`).
#' @param accent Character. Hex code for accent/highlight color. Defaults to
#' [civilytics_colors]`["ember_600"]` (`#C25311`).
#' @param strip_color Character. Hex code for facet strip background. Defaults
#' 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).
#'
#' @section Font size hierarchy:
#' All text sizes are derived from `font_size` using relative scale factors.
#' At the default `font_size = 14`:
#'
#' | Element | Scale factor | Default size |
#' |:--------|:-------------|:-------------|
#' | Plot title | `rel_large` (1.43x) | ~20 pt |
#' | Subtitle | 1.0x | 14 pt |
#' | Axis text (tick labels) | `rel_small` (0.86x) | ~12 pt |
#' | Axis titles | `rel_small` (0.86x) | ~12 pt |
#' | Legend text | `rel_small` (0.86x) | ~12 pt |
#' | Caption | `rel_tiny` (0.79x) | ~11 pt |
#' | Legend title | `rel_tiny` (0.79x) | ~11 pt |
#' | Strip text (facets) | `rel_small` (0.86x) | ~12 pt |
#'
#' To uniformly scale all text, change `font_size`. To adjust only the title
#' prominence, change `rel_large`. When using [civilytics_logo()] to add a
#' logo below the plot, pass `font_scale` to compensate for viewport
#' shrinkage.
#'
#' @return A complete ggplot2 [ggplot2::theme()] object.
#' @export
theme_civilytics <-
function (font_size = 14,
font_family = "",
line_size = 0.5,
rel_small = 12 / 14,
rel_tiny = 11 / 14,
rel_large = 16 / 14) {
half_line <- font_size / 2
small_size <- rel_small * font_size
theme_grey(base_size = font_size, base_family = font_family) %+replace%
theme(
line = element_line(
color = "black",
linewidth = line_size,
linetype = 1,
lineend = "butt"
),
rect = element_rect(
fill = NA,
color = NA,
linewidth = line_size,
linetype = 1
),
text = element_text(
family = font_family,
face = "plain",
color = "black",
size = font_size,
hjust = 0.5,
vjust = 0.5,
angle = 0,
lineheight = 0.9,
margin = margin(),
debug = FALSE
),
axis.line = element_line(
color = "black",
linewidth = line_size,
lineend = "square"
),
axis.line.x = NULL,
axis.line.y = NULL,
axis.text = element_text(color = "black",
size = small_size),
axis.text.x = element_text(margin = margin(t = small_size / 4),
vjust = 1),
axis.text.x.top = element_text(margin = margin(b = small_size / 4),
vjust = 0),
axis.text.y = element_text(margin = margin(r = small_size / 4),
hjust = 1),
axis.text.y.right = element_text(margin = margin(l = small_size / 4),
hjust = 0),
axis.ticks = element_line(color = "black",
linewidth = line_size),
axis.ticks.length = unit(half_line / 2,
"pt"),
axis.title.x = element_text(margin = margin(t = half_line / 2),
vjust = 1),
axis.title.x.top = element_text(margin = margin(b = half_line / 2),
vjust = 0),
axis.title.y = element_text(
angle = 90,
margin = margin(r = half_line /
2),
vjust = 1
),
axis.title.y.right = element_text(
angle = -90,
margin = margin(l = half_line / 2),
vjust = 0
),
legend.background = element_blank(),
legend.spacing = unit(font_size, "pt"),
legend.spacing.x = NULL,
legend.spacing.y = NULL,
legend.margin = margin(0,
0, 0, 0),
legend.key = element_blank(),
legend.key.size = unit(1.1 *
font_size, "pt"),
legend.key.height = NULL,
legend.key.width = NULL,
legend.text = element_text(size = rel(rel_small)),
legend.text.align = NULL,
legend.title = element_text(hjust = 0),
legend.title.align = NULL,
legend.position = "right",
legend.direction = NULL,
legend.justification = c("left",
"center"),
legend.box = NULL,
legend.box.margin = margin(0,
0, 0, 0),
legend.box.background = element_blank(),
legend.box.spacing = unit(font_size, "pt"),
panel.background = element_blank(),
panel.border = element_blank(),
panel.grid = element_blank(),
panel.grid.major = NULL,
panel.grid.minor = NULL,
panel.grid.major.x = NULL,
panel.grid.major.y = NULL,
panel.grid.minor.x = NULL,
panel.grid.minor.y = NULL,
panel.spacing = unit(half_line,
"pt"),
panel.spacing.x = NULL,
panel.spacing.y = NULL,
panel.ontop = FALSE,
strip.background = element_rect(fill = "grey80"),
strip.text = element_text(
size = rel(rel_small),
margin = margin(half_line / 2, half_line /
2, half_line / 2,
half_line / 2)
),
strip.text.x = NULL,
strip.text.y = element_text(angle = -90),
strip.placement = "inside",
strip.placement.x = NULL,
strip.placement.y = NULL,
strip.switch.pad.grid = unit(half_line / 2,
"pt"),
strip.switch.pad.wrap = unit(half_line / 2,
"pt"),
plot.background = element_blank(),
plot.title = element_text(
face = "bold",
size = rel(rel_large),
hjust = 0,
vjust = 1,
margin = margin(b = half_line)
),
plot.subtitle = element_text(
size = rel(rel_small),
hjust = 0,
vjust = 1,
margin = margin(b = half_line)
),
plot.caption = element_text(
size = rel(rel_tiny),
hjust = 0, # set hjust to 0
vjust = 1,
lineheight = 1,
margin = margin(t = half_line)
),
plot.tag = element_text(
face = "bold",
hjust = 0,
vjust = 0.7
),
plot.tag.position = c(0, 1),
plot.margin = margin(half_line,
half_line, half_line, half_line),
complete = TRUE
#'
#' @examples
#' \dontrun{
#' library(ggplot2)
#'
#' # Default editorial theme
#' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics()
#'
#' # With both gridlines and brand colors
#' ggplot(mpg, aes(displ, hwy, colour = class)) +
#' geom_point() +
#' scale_color_civilytics() +
#' theme_civilytics(grid = "both")
#'
#' # Transparent background for embedding
#' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics(paper_bg = FALSE)
#'
#' # Larger text for poster or display
#' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics(font_size = 18)
#' }
theme_civilytics <- 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"]),
grid = c("y", "x", "both", "none"),
paper_bg = TRUE) {
grid <- match.arg(grid)
half_line <- font_size / 2
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(
base_size = font_size,
base_family = font_family,
ink = ink,
paper = paper,
accent = accent
) %+replace%
ggplot2::theme(
line = ggplot2::element_line(
color = ink,
linewidth = line_size,
linetype = 1,
lineend = "butt"
),
rect = ggplot2::element_rect(
fill = NA,
color = NA,
linewidth = line_size,
linetype = 1
),
text = ggplot2::element_text(
family = font_family,
face = "plain",
color = ink,
size = font_size,
hjust = 0.5,
vjust = 0.5,
angle = 0,
lineheight = 0.9,
margin = ggplot2::margin(),
debug = FALSE
),
# -- Axes --
axis.line = ggplot2::element_blank(),
axis.line.x = ggplot2::element_line(
color = ink,
linewidth = 0.6,
lineend = "square"
),
axis.line.y = ggplot2::element_blank(),
axis.text = ggplot2::element_text(
color = ink_2,
size = ggplot2::rel(rel_small)
),
axis.text.x = ggplot2::element_text(
margin = ggplot2::margin(t = small_size / 4),
vjust = 1
),
axis.text.x.top = ggplot2::element_text(
margin = ggplot2::margin(b = small_size / 4),
vjust = 0
),
axis.text.y = ggplot2::element_text(
margin = ggplot2::margin(r = small_size / 4),
hjust = 1
),
axis.text.y.right = ggplot2::element_text(
margin = ggplot2::margin(l = small_size / 4),
hjust = 0
),
axis.ticks = ggplot2::element_line(
color = ink_3,
linewidth = 0.4
),
axis.ticks.length = ggplot2::unit(4, "pt"),
axis.title.x = ggplot2::element_text(
size = ggplot2::rel(rel_small),
color = ink_3,
margin = ggplot2::margin(t = 10),
vjust = 1
),
axis.title.x.top = ggplot2::element_text(
size = ggplot2::rel(rel_small),
color = ink_3,
margin = ggplot2::margin(b = half_line / 2),
vjust = 0
),
axis.title.y = ggplot2::element_text(
size = ggplot2::rel(rel_small),
color = ink_3,
angle = 90,
margin = ggplot2::margin(r = 10),
vjust = 1
),
axis.title.y.right = ggplot2::element_text(
size = ggplot2::rel(rel_small),
color = ink_3,
angle = -90,
margin = ggplot2::margin(l = half_line / 2),
vjust = 0
),
# -- Legend --
legend.background = ggplot2::element_blank(),
legend.spacing = ggplot2::unit(font_size, "pt"),
legend.spacing.x = NULL,
legend.spacing.y = NULL,
legend.margin = ggplot2::margin(0, 0, 4, 0),
legend.key = ggplot2::element_blank(),
legend.key.size = ggplot2::unit(12, "pt"),
legend.key.height = NULL,
legend.key.width = NULL,
legend.text = ggplot2::element_text(
size = ggplot2::rel(rel_small),
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.justification = c("left", "center"),
legend.box = NULL,
legend.box.margin = ggplot2::margin(0, 0, 0, 0),
legend.box.background = ggplot2::element_blank(),
legend.box.spacing = ggplot2::unit(font_size, "pt"),
# -- Panel --
panel.background = ggplot2::element_rect(fill = bg_color, color = NA),
panel.border = ggplot2::element_blank(),
panel.grid.minor = ggplot2::element_blank(),
panel.grid.major.x = if (grid %in% c("x", "both")) grid_line else no_line,
panel.grid.major.y = if (grid %in% c("y", "both")) grid_line else no_line,
panel.spacing = ggplot2::unit(16, "pt"),
panel.spacing.x = NULL,
panel.spacing.y = NULL,
panel.ontop = FALSE,
# -- Facet strips --
strip.background = ggplot2::element_rect(fill = strip_color, color = NA),
strip.text = ggplot2::element_text(
family = font_family,
face = "bold",
size = ggplot2::rel(rel_small),
color = ink,
margin = ggplot2::margin(
half_line / 2, half_line / 2,
half_line / 2, half_line / 2
)
),
strip.text.x = NULL,
strip.text.y = ggplot2::element_text(angle = -90),
strip.placement = "inside",
strip.placement.x = NULL,
strip.placement.y = NULL,
strip.switch.pad.grid = ggplot2::unit(half_line / 2, "pt"),
strip.switch.pad.wrap = ggplot2::unit(half_line / 2, "pt"),
# -- Plot-level --
plot.background = ggplot2::element_rect(fill = bg_color, color = NA),
plot.title = ggplot2::element_text(
family = title_family,
face = "bold",
size = ggplot2::rel(rel_large),
hjust = 0,
vjust = 1,
margin = ggplot2::margin(b = 4)
),
plot.title.position = "plot",
plot.subtitle = ggplot2::element_text(
size = ggplot2::rel(1),
color = ink_2,
hjust = 0,
vjust = 1,
lineheight = 1.3,
margin = ggplot2::margin(b = 14)
),
plot.caption = ggplot2::element_text(
size = ggplot2::rel(rel_tiny),
color = ink_3,
hjust = 0,
vjust = 1,
lineheight = 1.3,
margin = ggplot2::margin(t = 14)
),
plot.caption.position = "plot",
plot.tag = ggplot2::element_text(
face = "bold",
color = accent,
size = ggplot2::rel(rel_tiny),
hjust = 0,
vjust = 0.7
),
plot.tag.position = c(0, 1),
plot.margin = ggplot2::margin(16, 18, 16, 16),
complete = TRUE
)
}
#' Dark variant of the Civilytics ggplot2 theme
#'
#' Convenience wrapper around [theme_civilytics()] with dark-background
#' defaults: navy (`#1A2E4A`) paper, warm off-white (`#FAF7F2`) ink, and
#' a lighter ember accent (`#E07840`). Facet strips use primary navy.
#'
#' Pair with `make_logo_grob(variant = "dark")` for the white logo.
#'
#' @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_dark()
#' }
theme_civilytics_dark <- 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"]),
grid = c("y", "x", "both", "none"),
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 = 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"])
)
}
)
}
#' 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 = 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"]),
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)
)
}
# -- 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()
}
+27 -7
View File
@@ -149,6 +149,9 @@ race_short_names <- function(x) {
#' Sum a numeric that contains missing values and ignore missing values
#'
#' @param x a numeric vector
#' @param quiet Logical. If `TRUE` (default `FALSE`), suppress the warning
#' message. Useful when calling `na_sum()` inside a loop or `dplyr` pipeline
#' where the message would be emitted repeatedly.
#'
#' @return the sum, ignoring any missing values
#' @export
@@ -156,9 +159,12 @@ race_short_names <- function(x) {
#' @examples
#' x <- c(2, NA, 4, 9)
#' na_sum(x) # 15
na_sum <- function(x) {
#' na_sum(x, quiet = TRUE) # 15 (no message)
na_sum <- function(x, quiet = FALSE) {
stopifnot(is.numeric(x))
message("Taking a sum with missing values equal to 0, be careful!")
if (!quiet) {
message("Taking a sum with missing values equal to 0, be careful!")
}
x <- na_zero(x)
return(sum(x))
}
@@ -171,7 +177,6 @@ na_sum <- function(x) {
#' Title
#'
#' @param x a vector of state names
## #' @importFrom datasets state.abb state.name
#' @return state abbreviations matching state naems provided in X
#' @export
#'
@@ -234,14 +239,21 @@ outersect <- function(x, y, ...) {
#' @param stabbr a two letter abbreviation for a US state
#'
#' @return FIPS codes that match the abbreviation
#' @import tidycensus
#' @export
#'
#' @examples
#' \dontrun{
#' get_fips("MT")
#' get_fips("PR")
#' get_fips("CC")
#' }
get_fips <- function(stabbr) {
if (!requireNamespace("tidycensus", quietly = TRUE)) {
stop(
"Package 'tidycensus' is required for get_fips(). ",
"Install it with: install.packages('tidycensus')",
call. = FALSE
)
}
fips <- tidycensus::fips_codes[, 1:2]
fips <- fips[!duplicated(fips),]
out <- fips[fips$state == stabbr, 2]
@@ -253,12 +265,20 @@ get_fips <- function(stabbr) {
#' @param fips a character value that captures the FIPS code with leading 0
#'
#' @return a character value, length 2, with the state abbreviation
#' @import tidycensus
#' @export
#'
#' @examples
#' \dontrun{
#' get_stabbr("06")
#' }
get_stabbr <- function(fips) {
if (!requireNamespace("tidycensus", quietly = TRUE)) {
stop(
"Package 'tidycensus' is required for get_stabbr(). ",
"Install it with: install.packages('tidycensus')",
call. = FALSE
)
}
fips_codes <- tidycensus::fips_codes[, 1:2]
fips_codes <- fips_codes[!duplicated(fips_codes),]
if (length(fips) != 1) {
@@ -442,6 +462,6 @@ round_to_nearest_half <- function(x) {
#' @examples
#' rnh(c(0.2, 0.3, 0.4, 0.8, 0.09, 0.9))
rnh <- function(x) {
tmp <- Vectorize(civilytics::round_to_nearest_half)
tmp <- Vectorize(round_to_nearest_half)
tmp(x)
}
+284
View File
@@ -0,0 +1,284 @@
---
output: github_document
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.path = "man/figures/README-",
dpi = 96,
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_git("https://gitea.civilytics.org/Civilytics/civilyticsR.git")
```
## 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()`.
The logo is automatically right-aligned below the plot area. Wrap the
ggplot chain in parentheses before piping --- R's `|>` binds tighter
than `+`.
### Wordmark on a light plot
```{r logo-wordmark, fig.height = 4.5, fig.width = 7}
p <- ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point(size = 2) +
scale_color_civilytics() +
labs(
title = "Fuel economy by engine displacement",
caption = "Source: EPA fuel economy data (ggplot2::mpg)",
x = "Displacement (L)", y = "Highway MPG"
) +
theme_civilytics()
grid::grid.draw(civilytics_logo(p))
```
### Compact mark
Use `type = "mark"` for the compact C-pulse icon instead of the full
wordmark.
```{r logo-mark, fig.height = 4.5, fig.width = 7}
p2_logo <- ggplot(mpg, aes(displ, hwy, colour = factor(cyl))) +
geom_point(size = 2) +
scale_color_civilytics() +
labs(
title = "Engine size vs. highway fuel economy",
colour = "Cylinders",
x = "Displacement (L)", y = "Highway MPG"
) +
theme_civilytics()
grid::grid.draw(civilytics_logo(p2_logo, type = "mark"))
```
### Multi-plot layout with logo
Use `add_logo_ga()` to attach a single logo below a row of plots.
```{r logo-multi, fig.height = 4.5, fig.width = 9}
p1 <- ggplot(mpg, aes(class, fill = class)) +
geom_bar(show.legend = FALSE) +
scale_fill_civilytics() +
labs(title = "Vehicle counts", x = NULL, y = NULL) +
theme_civilytics(grid = "y")
p2 <- ggplot(mpg, aes(displ, hwy)) +
geom_point(colour = civilytics_colors["navy_600"], size = 1.5) +
labs(title = "Displacement vs. MPG", x = "Displacement (L)", y = "Highway MPG") +
theme_civilytics(grid = "y")
logo <- make_logo_grob()
grid::grid.draw(add_logo_ga(list(p1, p2), 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 |
## Maintaining brand assets
Logo and brand mark files live in `inst/img/`. The package ships both
PNG (for ggplot2 raster composition) and SVG (for Quarto/HTML output)
variants:
| File | Format | Used by |
|:-----|:-------|:--------|
| `civilytics-wordmark.png` / `.svg` | Full "Civilytics" lockup | `make_logo_grob("wordmark", "light")`, Quarto templates |
| `civilytics-wordmark-reverse.png` / `.svg` | Light-on-dark wordmark | `make_logo_grob("wordmark", "dark")`, dark slides |
| `civilytics-mark.png` / `.svg` | Compact C-pulse icon | `make_logo_grob("mark", "light")` |
| `civilytics-mark-reverse.svg` | Light-on-dark mark | `make_logo_grob("mark", "dark")` |
| `civilytics-pulse.svg` | Standalone waveform glyph | Quarto slide footer chrome |
To update the logos, replace the files in `inst/img/` with new versions
using the same filenames. The PNG files must be raster images (the
ggplot2 logo functions read them via `png::readPNG()`). SVG files are
passed through as-is by Quarto and HTML templates.
After replacing files, re-render the README gallery to update the
screenshots:
```r
devtools::load_all()
rmarkdown::render("README.Rmd")
```
+243 -10
View File
@@ -1,25 +1,258 @@
# civilytics
<!-- badges: start -->
<!-- badges: end -->
The goal of civilytics is to ...
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
You can install the released version of civilytics from [CRAN](https://CRAN.R-project.org) with:
Install from the Civilytics Gitea server:
``` r
install.packages("civilytics")
# install.packages("remotes")
remotes::install_git("https://gitea.civilytics.org/Civilytics/civilyticsR.git")
```
## Example
This is a basic example which shows you how to solve a common problem:
## Quick start
``` r
library(civilytics)
## basic example code
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()`. The logo is automatically right-aligned below the
plot area. Wrap the ggplot chain in parentheses before piping — R’s `|>`
binds tighter than `+`.
### Wordmark on a light plot
``` r
p <- ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point(size = 2) +
scale_color_civilytics() +
labs(
title = "Fuel economy by engine displacement",
caption = "Source: EPA fuel economy data (ggplot2::mpg)",
x = "Displacement (L)", y = "Highway MPG"
) +
theme_civilytics()
grid::grid.draw(civilytics_logo(p))
```
<img src="man/figures/README-logo-wordmark-1.png" alt="" width="100%" />
### Compact mark
Use `type = "mark"` for the compact C-pulse icon instead of the full
wordmark.
``` r
p2_logo <- ggplot(mpg, aes(displ, hwy, colour = factor(cyl))) +
geom_point(size = 2) +
scale_color_civilytics() +
labs(
title = "Engine size vs. highway fuel economy",
colour = "Cylinders",
x = "Displacement (L)", y = "Highway MPG"
) +
theme_civilytics()
grid::grid.draw(civilytics_logo(p2_logo, type = "mark"))
```
<img src="man/figures/README-logo-mark-1.png" alt="" width="100%" />
### Multi-plot layout with logo
Use `add_logo_ga()` to attach a single logo below a row of plots.
``` r
p1 <- ggplot(mpg, aes(class, fill = class)) +
geom_bar(show.legend = FALSE) +
scale_fill_civilytics() +
labs(title = "Vehicle counts", x = NULL, y = NULL) +
theme_civilytics(grid = "y")
p2 <- ggplot(mpg, aes(displ, hwy)) +
geom_point(colour = civilytics_colors["navy_600"], size = 1.5) +
labs(title = "Displacement vs. MPG", x = "Displacement (L)", y = "Highway MPG") +
theme_civilytics(grid = "y")
logo <- make_logo_grob()
grid::grid.draw(add_logo_ga(list(p1, p2), logo))
```
<img src="man/figures/README-logo-multi-1.png" alt="" width="100%" />
## 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 |
## Maintaining brand assets
Logo and brand mark files live in `inst/img/`. The package ships both
PNG (for ggplot2 raster composition) and SVG (for Quarto/HTML output)
variants:
| File | Format | Used by |
|:---|:---|:---|
| `civilytics-wordmark.png` / `.svg` | Full “Civilytics” lockup | `make_logo_grob("wordmark", "light")`, Quarto templates |
| `civilytics-wordmark-reverse.png` / `.svg` | Light-on-dark wordmark | `make_logo_grob("wordmark", "dark")`, dark slides |
| `civilytics-mark.png` / `.svg` | Compact C-pulse icon | `make_logo_grob("mark", "light")` |
| `civilytics-mark-reverse.svg` | Light-on-dark mark | `make_logo_grob("mark", "dark")` |
| `civilytics-pulse.svg` | Standalone waveform glyph | Quarto slide footer chrome |
To update the logos, replace the files in `inst/img/` with new versions
using the same filenames. The PNG files must be raster images (the
ggplot2 logo functions read them via `png::readPNG()`). SVG files are
passed through as-is by Quarto and HTML templates.
After replacing files, re-render the README gallery to update the
screenshots:
``` r
devtools::load_all()
rmarkdown::render("README.Rmd")
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="Civilytics mark">
<rect width="100" height="100" fill="#0E1A2B"></rect>
<g fill="none" stroke="#FAF7F2" stroke-linecap="butt">
<path d="M 82.77 27.06 A 40 40 0 1 0 82.77 72.94" stroke-width="15"></path>
<path d="M 30 50 L 35 50 L 38 44 L 42 56 L 46 42 L 50 58 L 54 50 L 64 50" stroke-width="3.5" stroke-linecap="square"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="Civilytics mark">
<g fill="none" stroke="#0E1A2B" stroke-linecap="butt">
<path d="M 82.77 27.06 A 40 40 0 1 0 82.77 72.94" stroke-width="15"></path>
<path d="M 30 50 L 35 50 L 38 44 L 42 56 L 46 42 L 50 58 L 54 50 L 64 50" stroke-width="3.5" stroke-linecap="square"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 385 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 14" role="img" aria-label="Civic pulse">
<path d="M0 8 L5 8 L8 3 L12 13 L16 1 L20 11 L23 8 L30 8" fill="none" stroke="#0E1A2B" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"></path>
</svg>

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

+65
View File
@@ -0,0 +1,65 @@
# Quarto 1.5+ brand file
# Optional but recommended — Quarto auto-applies these to HTML, PDF, and Revealjs.
# https://quarto.org/docs/authoring/brand.html
meta:
name: Civilytics Consulting
description: Turning public data into clear, actionable analysis for public good.
logo:
small: assets/logo/civilytics-mark.svg
medium: assets/logo/civilytics-wordmark.svg
large: assets/logo/civilytics-wordmark.svg
color:
palette:
paper: "#FAF7F2"
paper-2: "#F2EDE4"
ink: "#0E1A2B"
ink-2: "#2B3A52"
ink-3: "#5A6A82"
navy: "#22406A"
ember: "#C25311"
ember-2: "#923D00"
teal: "#1F6F70"
plum: "#6B3A5E"
moss: "#4A6B2F"
brass: "#B8751C"
background: paper
foreground: ink
primary: navy
secondary: ember
success: moss
info: navy
warning: "#9A5F18"
danger: "#A6271D"
light: paper-2
dark: ink
link: navy
typography:
fonts:
- family: "Source Serif 4"
source: google
- family: "Inter"
source: google
- family: "JetBrains Mono"
source: google
- family: "Libre Franklin"
source: google
base:
family: Inter
size: 1rem
line-height: 1.65
headings:
family: "Libre Franklin"
weight: 800
style: normal
line-height: 1.08
color: ink
monospace:
family: "JetBrains Mono"
size: 0.92em
link:
color: navy
decoration: underline
+82
View File
@@ -0,0 +1,82 @@
---
title: "Who pays when rent outpaces wages?"
subtitle: "A 12-county analysis of cost-burdened renter households, 2019–2024."
author:
- name: "Jared Knowles"
affiliation: "Civilytics Consulting"
date: "2026-04-15"
abstract: |
We combine ACS 5-year microdata with HUD Fair Market Rent estimates
to track changes in rent burden across 12 metropolitan counties.
Between 2019 and 2024, the share of renter households spending more
than 30% of income on housing rose by 6.4 percentage points; the
increase was concentrated in counties where median wages stagnated.
categories: [Housing, Affordability, ACS]
format:
html:
theme: ../theme/civilytics.scss
css: ../theme/extras.css
toc: true
toc-location: right
typst:
template-partials:
- ../typst/typst-template.typ
- ../typst/typst-show.typ
pdf:
include-in-header: ../latex/civilytics.tex
include-before-body: ../latex/civilytics-title.tex
execute:
echo: false
warning: false
---
## Findings at a glance
::: {.stat-callout}
::: {}
[6.4 pp]{.stat-num}
[Increase in rent-burdened share, 2019→2024]{.stat-label .accent}
:::
::: {}
[1 in 2]{.stat-num}
[Renters in the studied counties now cost-burdened]{.stat-label}
:::
::: {}
[$412]{.stat-num}
[Median monthly rent gap, lowest-wage quintile]{.stat-label}
:::
:::
[Methodology note]{.eyebrow}
## Background
Rent has outpaced wages in every county we studied. In some, the gap is
modest and recent; in others, it has been widening for a decade. This
report disaggregates the trend along three lines: county, income
quintile, and household composition.
```{r}
#| label: fig-trend
#| fig-cap: "Cost-burdened renter share, 12 metro counties, 2019–2024."
#| fig-width: 7
#| fig-height: 4.2
library(ggplot2); library(civilytics)
ggplot(economics, aes(date, unemploy / pop)) +
geom_line(color = "#C25311", linewidth = 1) +
labs(title = "Cost-burdened renter share, 2019–2024",
subtitle = "Share spending more than 30% of household income on rent",
x = NULL, y = NULL,
caption = "Source: ACS 5-yr microdata; Civilytics analysis.") +
scale_y_continuous(labels = scales::percent_format()) +
theme_civilytics()
```
> Across all 12 counties, the steepest increases came in places where
> median wages were already in the bottom quartile of the metro region.
## Recommendations
1. Targeted rental assistance keyed to local wage trajectories.
2. Quarterly publication of rent-burden indicators by county.
3. Coordinated reporting between HUD and county human-services offices.
+65
View File
@@ -0,0 +1,65 @@
---
title: "Quarterly Analysis"
subtitle: "A brief look at the numbers."
author: "Civilytics Consulting"
date: today
format: civilytics-reveal-revealjs
---
## Findings at a glance
::: {.columns}
::: {.column width="50%"}
[Key metric]{.stat-number}
- Point one
- Point two
- Point three
:::
::: {.column width="50%"}
```{r}
#| fig-width: 6
#| fig-height: 4
library(ggplot2)
library(civilytics)
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point(size = 2.5) +
scale_color_civilytics() +
theme_civilytics_slide() +
labs(title = "Engine Size vs Fuel Economy",
x = "Displacement (L)", y = "Highway MPG")
```
:::
:::
## Section divider {.section}
Big idea goes here.
## Data table
| County | Burden (%) | Change |
|:-------------|:----------:|-------:|
| Dane | 38.2 | +4.1 |
| Milwaukee | 52.7 | +7.3 |
| Hennepin | 41.5 | +3.9 |
: Cost-burdened renter share, selected counties.
## A blockquote slide {.pullquote}
> Rent has outpaced wages in every county we studied.
Civilytics Consulting, 2026
## Thank you {.thank-you}
Questions?
- jared@civilytics.com
- civilytics.com
@@ -0,0 +1,56 @@
title: Civilytics Reveal
author: Civilytics Consulting
version: 1.0.0
quarto-required: ">=1.4.0"
contributes:
formats:
revealjs:
# ---- Theme ----
theme: [default, civilytics.scss]
# ---- Slide geometry ----
width: 1280
height: 720
margin: 0
min-scale: 0.2
max-scale: 2.0
center: false
# ---- Navigation / chrome ----
controls: true
controls-layout: edges
progress: true
slide-number: "c/t"
hash-type: number
history: true
navigation-mode: linear
transition: fade
transition-speed: fast
background-transition: fade
# ---- Code ----
highlight-style: github
code-line-numbers: true
code-copy: true
code-overflow: wrap
# ---- Math ----
html-math-method: mathjax
# ---- Tables ----
tbl-cap-location: top
# ---- Links / fragments ----
link-external-newwindow: true
incremental: false
# ---- Brand chrome (injected by civilytics.js) ----
include-in-header:
- file: civilytics-head.html
include-after-body:
- file: civilytics-after.html
# ---- Footer / logo ----
# Override in the document front-matter if desired:
# footer: "Civilytics Consulting · 2026"
# logo: _extensions/civilytics-reveal/assets/civilytics-mark.svg
@@ -0,0 +1,80 @@
<!-- Civilytics Reveal — brand chrome injector -->
<script>
(function () {
// The Civilytics wordmark is the bold serif "Civilytics" lockup with the pulse
// waveform riding inline at the end. It ships as a baked SVG in two variants:
// ink-on-paper (default) and paper-on-ink (reverse) for dark backgrounds.
// We render it as an <img> so the official artwork is preserved exactly —
// the file picks up its color from the SVG itself, not from CSS currentColor.
var ASSET_BASE = (function () {
// Find the URL this script's <link rel="stylesheet" href="..civilytics.css..">
// was loaded from, so we can resolve sibling assets the same way.
var links = document.querySelectorAll('link[rel="stylesheet"]');
for (var i = 0; i < links.length; i++) {
var href = links[i].getAttribute('href') || '';
var m = href.match(/^(.*\/)civilytics\.css(\?|$)/);
if (m) return m[1] + 'assets/';
}
// Fallback: assume Quarto-style sibling layout
return '_extensions/civilytics-reveal/assets/';
})();
function makeWordmarkImg(reverse) {
var img = document.createElement('img');
img.className = 'civilytics-wordmark';
img.alt = 'Civilytics';
img.src = ASSET_BASE + (reverse ? 'civilytics-wordmark-reverse.svg' : 'civilytics-wordmark.svg');
return img;
}
function injectWordmark(slide, reverse) {
if (!slide) return;
if (slide.querySelector(':scope > .civilytics-wordmark')) return;
slide.insertBefore(makeWordmarkImg(reverse), slide.firstChild);
}
function init() {
if (!window.Reveal) { setTimeout(init, 50); return; }
var root = document.querySelector('.reveal');
if (!root) return;
// Title slide — paper background, ink wordmark
document.querySelectorAll(
'.slides #title-slide, .slides section.title-slide'
).forEach(function (s) { injectWordmark(s, false); });
// Thank-you slide — ink background, reversed wordmark
document.querySelectorAll('.slides section.thank-you').forEach(function (s) {
injectWordmark(s, true);
});
// Brand footer (pulse glyph + "Civilytics") bottom-left on body slides.
// Uses inline SVG so it inherits the slide's text color via currentColor.
if (!root.querySelector('.civilytics-footer')) {
var footer = document.createElement('div');
footer.className = 'civilytics-footer';
footer.setAttribute('aria-hidden', 'true');
footer.innerHTML =
'<svg viewBox="0 0 30 14" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter">' +
'<path d="M0 8 L5 8 L8 3 L12 13 L16 1 L20 11 L23 8 L30 8"/>' +
'</svg>' +
'<span>Civilytics</span>';
root.appendChild(footer);
}
function updateChrome() {
var current = Reveal.getCurrentSlide();
if (!current) return;
var hide = current.matches('.title-slide, #title-slide, .section-divider, .section, .thank-you, .full-bleed, .no-chrome');
document.body.classList.toggle('civilytics-hide-chrome', !!hide);
}
Reveal.on('ready', updateChrome);
Reveal.on('slidechanged', updateChrome);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
@@ -0,0 +1,3 @@
<!-- Civilytics Reveal — fonts preconnect (theme CSS also @imports, this just warms the connection) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
% Civilytics — title-page partial.
% Replaces Quarto's default \maketitle. Uses values from YAML
% (\thetitle, \theauthor, \thedate) plus an \ifabstract block.
% Guard: \thesubtitle is normally defined by civilytics.tex's subtitle
% capture; provide a fallback so this partial degrades gracefully if used
% without that preamble. See civilyticsR issue #13.
\providecommand{\thesubtitle}{}
\begin{titlepage}
\pagecolor{paper}
\color{ink}
\vspace*{0.5in}
{\sffamily\bfseries\scriptsize\color{ember}\MakeUppercase{— Civilytics Consulting}\par}
\vspace{12pt}
{\displayfont\fontsize{32pt}{34pt}\selectfont\bfseries\color{ink}\thetitle\par}
\vspace{8pt}
\ifx\thesubtitle\@empty\else
{\rmfamily\itshape\fontsize{14pt}{18pt}\selectfont\color{ink2}\thesubtitle\par}
\fi
\vspace{18pt}
\noindent\rule{\linewidth}{2pt}\par
\vspace{1pt}
\noindent{\color{rule}\rule{\linewidth}{0.4pt}}\par
\vspace{8pt}
\noindent
\begin{minipage}[t]{0.5\linewidth}
{\sffamily\scriptsize\color{ink3}\MakeUppercase{Authors}\par}
\vspace{2pt}
{\sffamily\small\color{ink}\theauthor\par}
\end{minipage}\hfill
\begin{minipage}[t]{0.4\linewidth}\raggedleft
{\sffamily\scriptsize\color{ink3}\MakeUppercase{Published}\par}
\vspace{2pt}
{\sffamily\small\color{ink}\thedate\par}
\end{minipage}\par
\vspace{1.5cm}
% Pulse mark, in ember
\begin{center}
{\color{ember}\rule{40pt}{2pt}}
\end{center}
\end{titlepage}
\clearpage
+135
View File
@@ -0,0 +1,135 @@
% =============================================================
% Civilytics — LaTeX preamble for Quarto PDF
% Usage in YAML:
% format:
% pdf:
% include-in-header: quarto/latex/civilytics.tex
% include-before-body: quarto/latex/civilytics-title.tex
% Requires: xelatex or lualatex (for system fonts).
% =============================================================
\usepackage{xcolor}
\usepackage{fontspec}
\usepackage{titlesec}
\usepackage{titling}
\usepackage{fancyhdr}
\usepackage{enumitem}
\usepackage{booktabs}
\usepackage{caption}
\usepackage[hidelinks]{hyperref}
\usepackage{microtype}
\usepackage{tcolorbox}
\tcbuselibrary{skins,breakable}
% --- Civilytics palette ---
\definecolor{paper}{HTML}{FAF7F2}
\definecolor{paper2}{HTML}{F2EDE4}
\definecolor{ink}{HTML}{0E1A2B}
\definecolor{ink2}{HTML}{2B3A52}
\definecolor{ink3}{HTML}{5A6A82}
\definecolor{rule}{HTML}{D6CEBD}
\definecolor{ruleStrong}{HTML}{B8AE97}
\definecolor{navy}{HTML}{22406A}
\definecolor{ember}{HTML}{C25311}
\definecolor{emberDark}{HTML}{923D00}
\definecolor{teal}{HTML}{1F6F70}
\definecolor{plum}{HTML}{6B3A5E}
% --- Page color ---
\pagecolor{paper}
\color{ink}
% --- Fonts (require local install or fontspec lookup) ---
% Bold uses the family's native Bold weight (present in every Source Serif 4
% install). Do NOT hard-require a "SemiBold" face: the package installs no
% system fonts for the PDF path, and standard Source Serif 4 ships only
% Regular/Bold/Italic/BoldItalic. See civilyticsR issue #14.
\setmainfont{Source Serif 4}[
UprightFont = *,
ItalicFont = * Italic,
Ligatures = TeX,
]
\setsansfont{Inter}[Ligatures = TeX]
\setmonofont{JetBrains Mono}[Scale = 0.92]
\newfontfamily\displayfont{Libre Franklin}[Ligatures = TeX]
% --- Subtitle capture ---
% Quarto/pandoc defines \subtitle (which appends to \@title) but never
% \thesubtitle, which the title page uses. This preamble is emitted before
% pandoc's \providecommand{\subtitle}, so our definition wins: capture the
% subtitle into \thesubtitle instead. See civilyticsR issue #13.
\makeatletter
\providecommand{\thesubtitle}{}
\def\subtitle#1{\renewcommand{\thesubtitle}{#1}}
\makeatother
% --- Use the Civilytics title page, not pandoc's default ---
% civilytics-title.tex (include-before-body) IS the title page. Quarto emits
% its default \maketitle + abstract *before* include-before-body, which would
% print a second, unstyled title. Neutralise both here, in the preamble
% (runs at \begin{document}, before the default title). The branded title page
% does not display the abstract. See civilyticsR issue #13.
\AtBeginDocument{%
\renewcommand{\maketitle}{}%
\renewenvironment{abstract}{\setbox0=\vbox\bgroup}{\egroup}%
}
% --- Hyperlinks ---
\hypersetup{
colorlinks = true,
linkcolor = navy,
urlcolor = navy,
citecolor = navy,
filecolor = navy,
}
% --- Section headings ---
\titleformat{\section}[block]
{\bfseries\Large\displayfont\color{ink}}
{}{0pt}
{\titlerule[0.4pt]\vspace{4pt}}[\vspace{-6pt}]
\titleformat{\subsection}[block]
{\bfseries\large\displayfont\color{ink}}{}{0pt}{}
\titleformat{\subsubsection}[block]
{\bfseries\normalsize\displayfont\color{ink}}{}{0pt}{}
% --- Page header / footer ---
\pagestyle{fancy}
\fancyhf{}
\renewcommand{\headrulewidth}{0pt}
\renewcommand{\footrulewidth}{0pt}
\fancyhead[L]{\sffamily\scriptsize\color{ink3}\MakeUppercase{Civilytics Consulting}}
\fancyhead[R]{\sffamily\scriptsize\color{ink3}\thetitle}
\fancyfoot[L]{\sffamily\scriptsize\color{ink3}civilytics.com}
\fancyfoot[C]{\sffamily\scriptsize\color{ink3}\thepage}
\fancyfoot[R]{\sffamily\scriptsize\color{ink3}\textcopyright\ 2026}
% --- Captions ---
\captionsetup{
font={sf,small,color=ink3},
labelfont={sf,bf,color=ink3},
labelsep=period,
justification=raggedright,
singlelinecheck=false,
}
% --- Pullquote ---
\newtcolorbox{pullquote}{
enhanced, breakable, frame hidden, colback=paper,
borderline west = {2pt}{0pt}{ember},
left=14pt, right=4pt, top=4pt, bottom=4pt,
fontupper={\itshape\rmfamily\large\color{ink}},
}
% --- Code blocks (Pandoc + listings or minted both inherit colors below) ---
\definecolor{codebg}{HTML}{0E1A2B}
\definecolor{codefg}{HTML}{FAF7F2}
% --- Lists tighter ---
\setlist{itemsep=2pt, topsep=4pt}
% --- Tabular numerals ---
\newcommand{\tnum}[1]{{\addfontfeature{Numbers={Tabular,Lining}}#1}}
% --- Ember accent rule above section starts ---
\newcommand{\emberrule}{{\color{ember}\rule{60pt}{3pt}}\par\vspace{4pt}}
+73
View File
@@ -0,0 +1,73 @@
/*-- scss:defaults --*/
// =============================================================
// Civilytics tokens — single source of truth for colors + type.
// Mirrored from colors_and_type.css so figures, themes, and
// preview pages stay in sync.
// =============================================================
// Neutrals — warm paper → civic ink
$paper: #FAF7F2 !default;
$paper-2: #F2EDE4 !default;
$paper-3: #E6DFD1 !default;
$rule: #D6CEBD !default;
$rule-strong: #B8AE97 !default;
$ink: #0E1A2B !default;
$ink-2: #2B3A52 !default;
$ink-3: #5A6A82 !default;
$ink-4: #8C97AB !default;
// Brand — Civic Navy
$navy-900: #0E1A2B; $navy-800: #132339; $navy-700: #1A2E4A;
$navy-600: #22406A; $navy-500: #2E5590; $navy-400: #4A74B0;
$navy-300: #7A9BCA; $navy-200: #B3C6E0; $navy-100: #DDE6F2; $navy-50: #EEF3FA;
// Accent — Ember
$accent-900: #451A00; $accent-800: #6B2B00; $accent-700: #923D00;
$accent-600: #C25311; $accent-500: #DB6C25; $accent-400: #EA8A49;
$accent-300: #F2A976; $accent-200: #F8C8A3; $accent-100: #FBE0C6; $accent-50: #FDF1E4;
// Supporting (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
$success: $moss-600; $warning: #9A5F18; $danger: #A6271D; $info: $navy-500;
// Bootstrap-friendly aliases (used by Quarto's HTML theme)
$body-bg: $paper !default;
$body-color: $ink !default;
$primary: $navy-600 !default;
$secondary: $accent-600 !default;
$success-bs: $success !default;
$info-bs: $info !default;
$warning-bs: $warning !default;
$danger-bs: $danger !default;
$light: $paper-2 !default;
$dark: $ink !default;
$link-color: $navy-600 !default;
$link-hover-color: $navy-700 !default;
// Type stacks
$font-display: 'Libre Franklin', 'Franklin Gothic', 'Inter', system-ui, sans-serif !default;
$font-serif: 'Source Serif 4', 'Source Serif Pro', Georgia, 'Times New Roman', serif !default;
$font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif !default;
$font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace !default;
$font-family-sans-serif: $font-sans !default;
$font-family-monospace: $font-mono !default;
$font-family-base: $font-sans !default;
// Sizing
$font-size-base: 1rem !default;
$line-height-base: 1.65 !default;
// Borders + radii
$border-color: $rule !default;
$border-radius: 4px !default;
$border-radius-sm: 2px !default;
$border-radius-lg: 8px !default;
$border-radius-xl: 14px !default;
+331
View File
@@ -0,0 +1,331 @@
/*-- scss:defaults --*/
// =============================================================
// Civilytics — Quarto HTML report + website theme
// Built on Bootstrap defaults via Quarto's SCSS pipeline.
// Inherits all tokens from _tokens.scss.
// =============================================================
@import "_tokens.scss";
// Quarto-specific defaults
$navbar-bg: $paper;
$navbar-fg: $ink;
$navbar-hl: $accent-600;
$footer-bg: $ink;
$footer-fg: $paper;
$code-bg: $paper-2;
$code-color: $accent-700;
$pre-bg: $ink;
$pre-color: $paper;
// Tighter table look
$table-border-color: $rule;
$table-color: $ink;
// Headings inherit display family + tight tracking
$h1-font-size: 2.875rem;
$h2-font-size: 2.125rem;
$h3-font-size: 1.625rem;
$h4-font-size: 1.25rem;
$headings-font-family: $font-display;
$headings-font-weight: 800;
$headings-line-height: 1.08;
$headings-color: $ink;
$callout-color-note: $navy-600;
$callout-color-tip: $teal-600;
$callout-color-caution: $accent-700;
$callout-color-warning: $warning;
$callout-color-important: $danger;
/*-- scss:rules --*/
// =============================================================
// Editorial layer — overrides Bootstrap + Quarto defaults so
// rendered .qmd files look like Civilytics publications, not
// generic blog posts.
// =============================================================
@import url('https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@500;600;700;800;900&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
body {
font-family: $font-sans;
font-size: 1rem;
line-height: 1.7;
color: $ink;
background: $paper;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
// Headings — display sans for headlines, tight tracking
h1, h2, h3, h4, h5, h6 {
font-family: $font-display;
letter-spacing: -0.025em;
text-wrap: balance;
color: $ink;
}
h1 { font-weight: 900; letter-spacing: -0.035em; line-height: 1.02; }
h2 { font-weight: 800; letter-spacing: -0.03em; margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid $rule; }
h3 { font-weight: 700; margin-top: 1.75rem; }
h4 { font-weight: 700; }
h5, h6 { font-family: $font-sans; font-weight: 600; text-transform: none; }
// Article-body prose — long-form reading uses Source Serif 4
.quarto-title-block { margin-bottom: 2.5rem; }
.quarto-title .title {
font-family: $font-display;
font-weight: 900;
font-size: 3.25rem;
line-height: 1.0;
letter-spacing: -0.04em;
margin-bottom: 0.75rem;
}
.quarto-title .subtitle {
font-family: $font-serif;
font-style: italic;
font-weight: 400;
font-size: 1.375rem;
color: $ink-2;
line-height: 1.4;
max-width: 60ch;
}
.quarto-title-meta {
font-family: $font-sans;
font-size: 0.8125rem;
color: $ink-3;
text-transform: uppercase;
letter-spacing: 0.08em;
border-top: 3px double $ink;
border-bottom: 1px solid $rule;
padding: 0.75rem 0;
margin-top: 1.5rem;
}
.quarto-title-meta .quarto-title-meta-heading { color: $ink-4; font-size: 0.6875rem; }
.quarto-title-meta .quarto-title-meta-contents { color: $ink; font-size: 0.875rem; text-transform: none; letter-spacing: 0; font-weight: 500; }
// Lead / abstract — italic serif lockup
.abstract, p.abstract {
font-family: $font-serif;
font-style: italic;
font-size: 1.25rem;
line-height: 1.5;
color: $ink;
border-left: 2px solid $accent-600;
padding-left: 1.25rem;
margin: 2rem 0 2.5rem;
max-width: 62ch;
}
// Body paragraphs in article
main p, main li {
font-family: $font-serif;
font-size: 1.0625rem;
line-height: 1.65;
color: $ink;
text-wrap: pretty;
}
main ul, main ol { padding-left: 1.5em; }
main li { margin-bottom: 0.4em; }
// Links — civic navy, thickening underline
a { color: $navy-600; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; transition: color 120ms ease; }
a:hover { color: $navy-700; text-decoration-thickness: 2px; }
a:visited { color: $plum-600; }
// Pullquote / blockquote
blockquote {
font-family: $font-serif;
font-style: italic;
font-size: 1.625rem;
line-height: 1.4;
color: $ink;
border-left: 3px solid $accent-600;
padding: 0.5rem 0 0.5rem 1.5rem;
margin: 2rem 0;
}
blockquote p { font-family: inherit !important; font-size: inherit !important; line-height: inherit !important; }
// Code — light by default
code, kbd, samp {
font-family: $font-mono;
font-size: 0.92em;
font-feature-settings: "tnum" 1, "zero" 1;
}
code:not(pre code) {
background: $paper-2;
color: $accent-700;
padding: 0.1em 0.4em;
border-radius: 2px;
border: 1px solid $rule;
}
// Code blocks — light variant (default), with a dark-emphasis option below
div.sourceCode, pre.sourceCode {
background: $paper-2;
border: 1px solid $rule;
border-radius: 8px;
}
div.sourceCode pre {
background: transparent;
color: $ink;
padding: 1rem 1.25rem;
font-size: 0.875rem;
line-height: 1.55;
margin: 0;
}
// Highlight tokens (Quarto/Pandoc default classes)
pre.sourceCode {
.kw { color: $navy-700; font-weight: 600; } // keyword
.dt { color: $teal-600; } // data type
.st { color: $accent-700; } // string
.co { color: $ink-3; font-style: italic; } // comment
.fu { color: $navy-600; } // function
.op, .sc { color: $ink-2; } // operator
.dv, .fl { color: $plum-600; } // numbers
.va { color: $brass-600; } // variable
}
// Dark variant — opt in by adding {.dark} to the code chunk
.cell.dark div.sourceCode,
div.sourceCode.dark,
pre.dark {
background: $ink;
border-color: $navy-800;
pre, code { color: $paper; }
.kw { color: lighten($navy-300, 5%); }
.dt { color: $teal-300; }
.st { color: $accent-300; }
.co { color: $ink-4; }
.fu { color: $navy-200; }
.op, .sc { color: $paper-3; }
.dv, .fl { color: $plum-300; }
}
// Tables — editorial grid
.table, table {
font-family: $font-sans;
font-size: 0.9375rem;
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
margin: 1.5rem 0;
}
.table thead th, table thead th {
font-family: $font-sans;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $ink-3;
border-top: 2px solid $ink;
border-bottom: 1px solid $rule-strong;
padding: 0.6rem 0.75rem;
text-align: left;
}
.table tbody td, table tbody td {
border-bottom: 1px solid $rule;
padding: 0.55rem 0.75rem;
color: $ink;
}
.table tbody tr:last-child td, table tbody tr:last-child td { border-bottom: 2px solid $ink; }
.table.numeric td, table .numeric { font-family: $font-mono; }
// Figures + captions
figure, .quarto-figure { margin: 2rem 0; }
figcaption, .figure-caption, .quarto-figcaption {
font-family: $font-sans;
font-size: 0.8125rem;
color: $ink-3;
line-height: 1.45;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid $rule;
text-align: left;
}
// Callouts — keep Quarto markup, restyle as editorial sidebars
.callout {
border-radius: 8px;
border: 1px solid $rule;
background: $paper-2;
margin: 1.5rem 0;
border-left: 3px solid $navy-600;
}
.callout-note { border-left-color: $navy-600; }
.callout-tip { border-left-color: $teal-600; }
.callout-caution { border-left-color: $accent-700; }
.callout-warning { border-left-color: $warning; }
.callout-important { border-left-color: $danger; }
.callout-title { font-family: $font-sans; font-weight: 600; font-size: 0.9375rem; color: $ink; text-transform: none; letter-spacing: 0; }
.callout-body p { font-family: $font-sans !important; font-size: 0.9375rem !important; line-height: 1.55 !important; }
// Eyebrow / kicker (Quarto category renders as `.quarto-categories`)
.quarto-categories {
display: inline-flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.quarto-categories .quarto-category {
font-family: $font-sans;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: $accent-600;
background: transparent;
border: none;
padding: 0;
}
.quarto-categories .quarto-category::before { content: "— "; color: $accent-600; }
// Navbar (used by website projects)
.navbar {
background: rgba(250, 247, 242, 0.92) !important;
backdrop-filter: blur(12px);
border-bottom: 1px solid $rule;
padding: 0.875rem 0;
}
.navbar-brand { font-family: $font-display; font-weight: 800; font-size: 1.125rem; letter-spacing: -0.02em; color: $ink !important; }
.navbar-nav .nav-link { font-family: $font-sans; font-weight: 500; font-size: 0.9375rem; color: $ink-2 !important; padding: 0.4rem 0.85rem !important; }
.navbar-nav .nav-link:hover, .navbar-nav .nav-link.active { color: $accent-600 !important; }
// Footer
.nav-footer, footer.footer {
background: $ink;
color: $paper;
font-family: $font-sans;
font-size: 0.875rem;
padding: 2rem 0;
border-top: 4px solid $accent-600;
}
.nav-footer a, footer.footer a { color: $paper; text-decoration: underline; text-decoration-thickness: 1px; }
// TOC — right-side sidebar
#TOC, .sidebar.toc-active, nav#TOC {
font-family: $font-sans;
font-size: 0.8125rem;
}
#TOC ul, nav#TOC ul { list-style: none; padding-left: 0.75rem; border-left: 1px solid $rule; }
#TOC li, nav#TOC li { margin: 0.25rem 0; }
#TOC a, nav#TOC a { color: $ink-3; text-decoration: none; }
#TOC a:hover, #TOC a.active, nav#TOC a:hover, nav#TOC a.active { color: $accent-600; border-left: 2px solid $accent-600; padding-left: 0.5rem; margin-left: -0.75rem; }
// HR — editorial divider
hr { border: none; border-top: 1px solid $rule; margin: 2.5rem 0; }
hr.section-break { border: none; height: 4px; background: linear-gradient(to right, $accent-600 60px, $rule 60px); margin: 3rem 0; }
// Listing pages (website blog/index)
.quarto-listing .listing-item {
background: white;
border: 1px solid $rule;
border-top: 3px solid $accent-600;
border-radius: 8px;
padding: 1.5rem;
transition: border-color 120ms ease;
}
.quarto-listing .listing-item:hover { border-color: $rule-strong; }
.quarto-listing .listing-title a { color: $ink; text-decoration: none; font-family: $font-display; font-weight: 800; }
.quarto-listing .listing-date { font-family: $font-sans; font-size: 0.75rem; color: $ink-3; text-transform: uppercase; letter-spacing: 0.08em; }
// Selection
::selection { background: $accent-200; color: $ink; }
+63
View File
@@ -0,0 +1,63 @@
/* Civilytics — extras layered on top of the SCSS theme.
Use for utilities that don't need to participate in the
Bootstrap variable cascade. */
/* Stat callout — drop into reports for hero numbers */
.stat-callout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 2rem;
margin: 2rem 0 2.5rem;
padding: 1.5rem 0;
border-top: 3px double #0E1A2B;
border-bottom: 1px solid #D6CEBD;
}
.stat-callout .stat-num {
font-family: 'Source Serif 4', Georgia, serif;
font-weight: 700;
font-size: 3rem;
line-height: 1;
letter-spacing: -0.02em;
color: #0E1A2B;
font-variant-numeric: tabular-nums;
display: block;
}
.stat-callout .stat-label {
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #5A6A82;
margin-top: 0.4rem;
}
.stat-callout .stat-label.accent { color: #C25311; }
/* Pulse glyph utility — matches design system */
.civilytics-pulse {
display: inline-block;
vertical-align: middle;
width: 30px;
height: 14px;
}
/* Section eyebrow — use above an h1/h2 */
.eyebrow {
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #C25311;
margin: 0 0 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.eyebrow::before {
content: "";
display: inline-block;
width: 24px;
height: 2px;
background: #C25311;
}
+17
View File
@@ -0,0 +1,17 @@
// Civilytics — Typst show/entry partial for Quarto (typst-show.typ).
// Pairs with typst-template.typ. Quarto appends the rendered document body
// after this partial, so this file intentionally ends with the show rule and
// no trailing body token. (Do not write that token in a comment here: Quarto
// interpolates its template variables even inside comments.)
// Title/subtitle/date are wrapped in [ ] so arbitrary text (including words
// that are Typst keywords like "for"/"in") is treated as content, not code.
// See civilyticsR issue #12.
#show: doc => civilytics(
title: [$title$],
$if(subtitle)$subtitle: [$subtitle$],$endif$
$if(by-author)$authors: ($for(by-author)$"$it.name.literal$",$endfor$),$endif$
$if(date)$date: [$date$],$endif$
$if(abstract)$abstract: [$abstract$],$endif$
toc: $if(toc)$true$else$false$endif$,
doc
)
+237
View File
@@ -0,0 +1,237 @@
// =============================================================
// Civilytics — Typst template partial for Quarto PDF (typst-template.typ).
// Shipped as a Quarto template-partial (paired with typst-show.typ) rather
// than a full `template:` so Quarto keeps its own `definitions` partial —
// which defines Skylighting/token functions needed for syntax-highlighted
// code blocks. See civilyticsR issue #12.
// Usage in YAML:
// format:
// typst:
// template-partials:
// - quarto/typst/typst-template.typ
// - quarto/typst/typst-show.typ
// =============================================================
#let paper-bg = rgb("#FAF7F2")
#let ink = rgb("#0E1A2B")
#let ink-2 = rgb("#2B3A52")
#let ink-3 = rgb("#5A6A82")
#let rule = rgb("#D6CEBD")
#let rule-strong = rgb("#B8AE97")
#let navy = rgb("#22406A")
#let ember = rgb("#C25311")
#let ember-dark = rgb("#923D00")
#let plum = rgb("#6B3A5E")
#let teal = rgb("#1F6F70")
#let serif-stack = ("Source Serif 4", "Source Serif Pro", "Georgia", "Times New Roman")
#let sans-stack = ("Inter", "Helvetica Neue", "Arial")
#let display-stack = ("Libre Franklin", "Inter", "Helvetica Neue")
#let mono-stack = ("JetBrains Mono", "Menlo", "Consolas")
#let civilytics(
title: none,
subtitle: none,
authors: (),
date: none,
abstract: none,
toc: true,
doc,
) = {
// Page setup
set page(
paper: "us-letter",
margin: (top: 1in, bottom: 1in, left: 1.1in, right: 1.1in),
fill: paper-bg,
header: context {
if counter(page).get().first() > 1 {
set text(font: sans-stack, size: 8pt, fill: ink-3, tracking: 0.06em)
upper[Civilytics Consulting]
h(1fr)
upper(if title != none { title } else { "" })
v(2pt)
line(length: 100%, stroke: 0.4pt + rule)
}
},
footer: context {
set text(font: sans-stack, size: 8pt, fill: ink-3)
line(length: 100%, stroke: 0.4pt + rule)
v(4pt)
grid(
columns: (1fr, auto, 1fr),
align: (left, center, right),
[civilytics.com],
counter(page).display("1 / 1", both: true),
[© 2026]
)
},
)
// Body text
set text(font: serif-stack, size: 10.5pt, fill: ink, lang: "en")
set par(justify: false, leading: 0.6em, first-line-indent: 0pt)
// Headings
show heading.where(level: 1): it => {
v(1.4em)
block[
#set text(font: display-stack, weight: 900, size: 22pt, fill: ink, tracking: -0.025em)
#it.body
]
v(0.2em)
line(length: 60pt, stroke: 3pt + ember)
v(0.6em)
}
show heading.where(level: 2): it => {
v(1.1em)
line(length: 100%, stroke: 0.5pt + rule)
v(0.5em)
block[
#set text(font: display-stack, weight: 800, size: 16pt, fill: ink, tracking: -0.02em)
#it.body
]
v(0.3em)
}
show heading.where(level: 3): it => {
v(0.8em)
block[
#set text(font: display-stack, weight: 700, size: 13pt, fill: ink, tracking: -0.015em)
#it.body
]
v(0.2em)
}
show heading.where(level: 4): it => block[
#set text(font: sans-stack, weight: 600, size: 11pt, fill: ink-2)
#it.body
]
// Links
show link: it => text(fill: navy, underline(it))
// Inline code
show raw.where(block: false): it => box(
fill: rgb("#F2EDE4"),
inset: (x: 3pt, y: 1pt),
outset: (y: 2pt),
radius: 1pt,
text(font: mono-stack, size: 0.92em, fill: ember-dark)[#it]
)
// Code blocks
show raw.where(block: true): it => block(
fill: ink,
width: 100%,
inset: 12pt,
radius: 4pt,
)[
#set text(font: mono-stack, size: 8.5pt, fill: paper-bg)
#it
]
// Block quote
show quote.where(block: true): it => block(
stroke: (left: 2pt + ember),
inset: (left: 14pt, top: 4pt, bottom: 4pt),
spacing: 1.2em,
)[
#set text(font: serif-stack, size: 13pt, style: "italic", fill: ink)
#it.body
]
// Figures
show figure: it => block(spacing: 1.4em)[
#it.body
#v(4pt)
#line(length: 100%, stroke: 0.4pt + rule)
#v(2pt)
#set text(font: sans-stack, size: 8.5pt, fill: ink-3)
#it.caption
]
// Tables
set table(
stroke: (x, y) => (
top: if y == 0 { 1.5pt + ink } else if y == 1 { 0.6pt + rule-strong } else { 0pt },
bottom: if y == 0 { 0pt } else { 0.4pt + rule },
),
inset: (x: 8pt, y: 6pt),
)
show table.cell.where(y: 0): set text(
font: sans-stack, size: 8pt, weight: 600, fill: ink-3, tracking: 0.06em
)
// ----- Title block -----
if title != none {
block[
#set text(font: sans-stack, size: 8pt, weight: 600, fill: ember, tracking: 0.1em)
#upper[— Civilytics Consulting]
]
v(8pt)
block[
#set text(font: display-stack, weight: 900, size: 32pt, fill: ink, tracking: -0.035em)
#set par(leading: 0.4em)
#title
]
if subtitle != none {
v(8pt)
block[
#set text(font: serif-stack, style: "italic", size: 14pt, fill: ink-2)
#set par(leading: 0.55em)
#subtitle
]
}
v(18pt)
line(length: 100%, stroke: 3pt + ink)
v(2pt)
line(length: 100%, stroke: 0.4pt + rule)
v(8pt)
// Authors + date row
grid(
columns: (1fr, 1fr),
align: (left, right),
[
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
#upper[Authors] \
#set text(font: sans-stack, size: 10pt, fill: ink, weight: 500, tracking: 0em)
#if authors.len() > 0 {
authors.map(a => if type(a) == str { a } else { a.name }).join(", ")
}
],
[
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
#upper[Published] \
#set text(font: sans-stack, size: 10pt, fill: ink, weight: 500, tracking: 0em)
#if date != none { date }
]
)
v(24pt)
if abstract != none {
block(
stroke: (left: 2pt + ember),
inset: (left: 14pt),
)[
#set text(font: serif-stack, style: "italic", size: 12pt, fill: ink)
#set par(leading: 0.55em)
#abstract
]
v(20pt)
}
if toc {
block[
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
#upper[Contents]
]
v(4pt)
line(length: 100%, stroke: 0.4pt + rule)
v(8pt)
outline(title: none, indent: auto, depth: 3)
v(20pt)
pagebreak()
}
}
doc
}
+16 -1
View File
@@ -4,7 +4,13 @@
\alias{add_logo}
\title{Add a logo to a ggplot2 object}
\usage{
add_logo(plot, logo, margin_param = NULL)
add_logo(
plot,
logo,
margin_param = NULL,
font_scale = 1.1,
position = c("bottom-right", "bottom-left", "top-right", "top-left")
)
}
\arguments{
\item{plot}{a ggplot2 grob}
@@ -12,6 +18,15 @@ add_logo(plot, logo, margin_param = NULL)
\item{logo}{a logo grob created by make_logo_grob()}
\item{margin_param}{a numeric specifying what margin to add or subtract to align the logo}
\item{font_scale}{Numeric. Multiplicative scaling factor applied to text
sizes before composing the plot with the logo. Default `1.1` inflates
text by ~10 \% to compensate for the viewport shrinkage caused by
[gridExtra::arrangeGrob()]. Set to `1` to disable.}
\item{position}{Character. Corner placement for the logo: `"bottom-right"`
(default), `"bottom-left"`, `"top-right"`, or `"top-left"`. Controls
whether the logo is placed above or below the plot.}
}
\value{
a grob with a logo attached to it ready to plot
+15 -3
View File
@@ -2,9 +2,16 @@
% Please edit documentation in R/logo.R
\name{add_logo_ga}
\alias{add_logo_ga}
\title{Add a logo to a ggplot2 object}
\title{Add a logo to multiple ggplot2 objects}
\usage{
add_logo_ga(plot_list, logo, nrow = 1, widths = NULL, margin_param = NULL)
add_logo_ga(
plot_list,
logo,
nrow = 1,
widths = NULL,
margin_param = NULL,
font_scale = 1.1
)
}
\arguments{
\item{plot_list}{a list containing ggplot2 objects}
@@ -16,17 +23,21 @@ add_logo_ga(plot_list, logo, nrow = 1, widths = NULL, margin_param = NULL)
\item{widths}{an optional vector the same length as plot_list with the widths for each plot}
\item{margin_param}{a number giving the adjustment up or down to help manually align logo and captions}
\item{font_scale}{Numeric. Multiplicative scaling factor applied to text
sizes before composing. Default `1.1`. See [add_logo()] for details.}
}
\value{
a grid object
}
\description{
Add a logo to a ggplot2 object
Add a logo to multiple ggplot2 objects
}
\note{
The resulting object needs to be drawn to the screen using grid.draw()
}
\examples{
\dontrun{
library(ggplot2); library(grid)
tmp_plot <- ggplot(mtcars) + aes(x = hp, y = disp) + geom_point() + theme_civilytics()
tmp_logo <- make_logo_grob()
@@ -34,3 +45,4 @@ plot_and_logo <- add_logo(tmp_plot, tmp_logo)
grid.draw(plot_and_logo)
dev.off()
}
}
+9 -5
View File
@@ -4,18 +4,22 @@
\alias{agresti_coull_interval}
\title{Calculate the Agresti-Coull interval}
\usage{
agresti_coull_interval(num, den, conf.level)
agresti_coull_interval(num, den, conf.level = 0.95)
}
\arguments{
\item{num}{a number of successes}
\item{num}{number of successes}
\item{den}{a number of trials}
\item{den}{number of trials}
\item{conf.level}{default 0.95, set the confidence interval to return}
\item{conf.level}{default 0.95, confidence level for the interval}
}
\value{
an interval
three values forming the lower bound, observed proportion, and upper bound
}
\description{
Calculate the Agresti-Coull interval
}
\examples{
agresti_coull_interval(20, 40)
agresti_coull_interval(2, 100, conf.level = 0.99)
}
+71
View File
@@ -0,0 +1,71 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/notifications.R
\name{beep}
\alias{beep}
\title{Send a CLI beep / desktop notification}
\usage{
beep(
msg = "done",
type = c("beep", "notify", "webhook", "all"),
url = NULL,
status = "done",
timeout = 5,
quiet = FALSE
)
}
\arguments{
\item{msg}{Character. Optional message to include in the notification.
When \code{type = "notify"}, this becomes the notification body.}
\item{type}{Character. Notification method:
\itemize{
\item \code{"beep"} (default): emit a terminal bell character.
Works in any terminal that supports the bell.
\item \code{"notify"}: send a desktop notification via \code{notify-send} (Linux) or
\code{osascript} (macOS). Falls back to \code{"beep"} if neither tool is found.
\item \code{"webhook"}: POST a JSON payload to a URL. Requires \code{url} argument.
\item \code{"all"}: play beep + send desktop notification (webhook only if \code{url}
is provided).
}}
\item{url}{Character. Webhook URL for \code{type = "webhook"} or \code{"all"}.
A JSON payload is POSTed with keys \code{message}, \code{status}, and \code{timestamp}.}
\item{status}{Character. Status label for the notification (default \code{"done"}).
Used in the notification title and webhook payload.}
\item{timeout}{Numeric. Seconds to wait for the webhook POST to complete
(default \code{5}). Ignored for non-webhook types.}
\item{quiet}{Logical. If \code{TRUE}, suppress the terminal beep even when
\code{type} includes \code{"beep"}. Useful for silent background runs.}
}
\value{
Invisible \code{NULL}.
}
\description{
Plays an audible beep in the terminal and/or sends a desktop notification
when a long-running R script completes or reaches a milestone.
}
\section{Requirements}{
\itemize{
\item \code{type = "notify"} requires \code{notify-send} (Linux) or \code{osascript} (macOS).
\item \code{type = "webhook"} requires network access to the provided URL.
}
}
\section{Examples}{
\preformatted{
# Simple terminal beep
beep()
# Desktop notification with message
beep("Analysis complete!", type = "notify")
# Send to a webhook (e.g., Slack, Discord, custom endpoint)
beep("Job finished", type = "webhook",
url = "https://hooks.slack.com/services/...")
# Beep + desktop notification
beep("Processing done", type = "all")
}
}
+45 -2
View File
@@ -4,8 +4,51 @@
\name{civilytics-package}
\alias{civilytics}
\alias{civilytics-package}
\title{civilytics: Utilities Functions for Civilytics}
\title{civilytics: Brand Themes, Color Palettes, and Utility Functions for Civilytics}
\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
}
}
\section{Quarto templates}{
\itemize{
\item [use_civilytics_revealjs()] -- install Reveal.js slide extension
\item [use_civilytics_theme()] -- install HTML/PDF/Typst document theme
\item [use_civilytics_brand()] -- install brand.yml only (Quarto 1.5+)
}
}
\seealso{
Useful links:
\itemize{
\item \url{https://gitea.civilytics.org/Civilytics/civilyticsR}
\item Report bugs at \url{https://gitea.civilytics.org/Civilytics/civilyticsR/issues}
}
}
\author{
\strong{Maintainer}: Jared E. Knowles \email{jared@civilytics.com}
}
\keyword{internal}
+23
View File
@@ -0,0 +1,23 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/colors.R
\docType{data}
\name{civilytics_colors}
\alias{civilytics_colors}
\title{Civilytics brand colors}
\format{
A named character vector of hex color codes.
}
\usage{
civilytics_colors
}
\description{
A named character vector of all Civilytics brand colors, matching the CSS
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{
civilytics_colors["ink"]
civilytics_colors[c("navy_600", "ember_600", "teal_600")]
}
\keyword{datasets}
+23
View File
@@ -0,0 +1,23 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/fonts.R
\name{civilytics_load_fonts}
\alias{civilytics_load_fonts}
\title{Load Civilytics brand fonts}
\usage{
civilytics_load_fonts()
}
\value{
Invisibly returns `NULL`.
}
\description{
Downloads Inter and Libre Franklin from Google Fonts via
[sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so
that all graphics devices render text with those fonts. This is called
automatically when the package loads; use this function to retry if the
initial load failed (e.g., the machine was offline at load time).
}
\examples{
\dontrun{
civilytics_load_fonts()
}
}
+87
View File
@@ -0,0 +1,87 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/logo.R
\name{civilytics_logo}
\alias{civilytics_logo}
\title{Add a Civilytics logo to a ggplot (pipe-friendly)}
\usage{
civilytics_logo(
plot,
type = c("wordmark", "mark"),
variant = c("light", "dark"),
position = c("bottom-right", "bottom-left", "top-right", "top-left"),
margin_param = NULL,
font_scale = 1.1
)
}
\arguments{
\item{plot}{A ggplot object.}
\item{type}{Character. `"wordmark"` (default) or `"mark"`. Passed to
[make_logo_grob()].}
\item{variant}{Character. `"light"` (default) or `"dark"`. Passed to
[make_logo_grob()].}
\item{position}{Character. Corner placement for the logo: `"bottom-right"`
(default), `"bottom-left"`, `"top-right"`, or `"top-left"`.}
\item{margin_param}{Numeric or `NULL`. Manual margin adjustment passed to
[add_logo()].}
\item{font_scale}{Numeric. Inflate text sizes by this factor to compensate
for viewport shrinkage when composing with [gridExtra::arrangeGrob()].
Default `1.1` (~10 \% inflation). Set to `1` to disable.}
}
\value{
A grob (from [gridExtra::arrangeGrob()]) ready to draw with
[grid::grid.draw()].
}
\description{
A convenience wrapper that creates the logo grob and attaches it to a
corner of the plot in one call. Designed for use with the base pipe `|>`.
}
\details{
**Important:** R's `|>` has *higher* precedence than `+`, so you must
wrap the ggplot chain in parentheses before piping:
```
(ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics()) |>
civilytics_logo()
```
}
\examples{
\dontrun{
library(ggplot2); library(grid)
# Pipe usage — parentheses required around the ggplot chain
(ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics()) |>
civilytics_logo() |>
grid.draw()
# Top-right placement
(ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics()) |>
civilytics_logo(position = "top-right") |>
grid.draw()
# Bottom-left with mark
(ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics()) |>
civilytics_logo(type = "mark", position = "bottom-left") |>
grid.draw()
# Dark theme with mark in top-left
(ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics_dark()) |>
civilytics_logo(variant = "dark", type = "mark",
position = "top-left") |>
grid.draw()
}
}
+24
View File
@@ -0,0 +1,24 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/colors.R
\name{civilytics_pal}
\alias{civilytics_pal}
\title{Civilytics palette function (closure)}
\usage{
civilytics_pal(name = "qual", reverse = FALSE)
}
\arguments{
\item{name}{Character. Palette name. See `names(civilytics_palettes)`.}
\item{reverse}{Logical. Reverse the palette order. Default `FALSE`.}
}
\value{
A function that takes integer `n` and returns `n` hex color codes.
}
\description{
Returns a closure `function(n)` suitable for passing to
[ggplot2::discrete_scale()] or similar scale constructors.
}
\examples{
pal_fn <- civilytics_pal("qual")
pal_fn(4)
}
+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}
+27
View File
@@ -0,0 +1,27 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/quarto.R
\name{.copy_logos}
\alias{.copy_logos}
\title{Copy logo SVGs from inst/img/ to a target directory}
\usage{
.copy_logos(
dest_dir,
path,
force,
files = c("civilytics-mark.svg", "civilytics-mark-reverse.svg",
"civilytics-wordmark.svg", "civilytics-wordmark-reverse.svg", "civilytics-pulse.svg")
)
}
\arguments{
\item{dest_dir}{Destination directory relative to `path`.}
\item{path}{Project root.}
\item{force}{Overwrite existing files?}
\item{files}{Character vector of logo filenames to copy.}
}
\description{
Copy logo SVGs from inst/img/ to a target directory
}
\keyword{internal}
+24
View File
@@ -0,0 +1,24 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/quarto.R
\name{.copy_pkg_file}
\alias{.copy_pkg_file}
\title{Copy a package file to a project directory}
\usage{
.copy_pkg_file(src, dst, path, force)
}
\arguments{
\item{src}{Relative path within the installed package (under `inst/`).}
\item{dst}{Destination path relative to `path`.}
\item{path}{Project root.}
\item{force}{Overwrite existing files?}
}
\value{
Invisible logical indicating success.
}
\description{
Copy a package file to a project directory
}
\keyword{internal}
+16
View File
@@ -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}
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+2 -1
View File
@@ -16,7 +16,8 @@ FIPS codes that match the abbreviation
Get the FIPS code for a given state abbreviation
}
\examples{
\dontrun{
get_fips("MT")
get_fips("PR")
get_fips("CC")
}
}
+2
View File
@@ -16,5 +16,7 @@ a character value, length 2, with the state abbreviation
Get the state abbreviation from a given FIPS Code
}
\examples{
\dontrun{
get_stabbr("06")
}
}
+1 -1
View File
@@ -16,6 +16,6 @@ a logical, TRUE if a caption exists and FALSE if it does not
Test whether a ggplot2 object has a caption
}
\examples{
p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
has_caption(p1) # FALSE
}
+25 -6
View File
@@ -4,16 +4,35 @@
\alias{make_logo_grob}
\title{Get a Civilytics Logo grob}
\usage{
make_logo_grob()
make_logo_grob(
type = c("wordmark", "mark"),
variant = c("light", "dark"),
position = c("bottom-right", "bottom-left", "top-right", "top-left")
)
}
\arguments{
\item{type}{Character. `"wordmark"` (default) uses the full wordmark.
`"mark"` uses the compact C-pulse icon only.}
\item{variant}{Character. `"light"` (default) uses the dark logo for light
backgrounds. `"dark"` uses the reverse (light) logo for dark backgrounds
(pairs with [theme_civilytics_dark()]).}
\item{position}{Character. Corner placement for the logo: `"bottom-right"`
(default), `"bottom-left"`, `"top-right"`, or `"top-left"`. Controls
horizontal alignment of the logo grob.}
}
\value{
a gg object which contains the logo file stored as a Grob suitable for manipulating in
grid
A ggplot object (class `"gg"`) containing the logo grob.
}
\description{
Get a Civilytics Logo grob
Returns a ggplot object containing the Civilytics logo as a rasterGrob,
ready to compose with plots via [add_logo()], [add_logo_ga()], or the
pipe-friendly [civilytics_logo()].
}
\examples{
logo <- make_logo_grob()
class(logo) # gg
logo <- make_logo_grob() # wordmark, light
logo <- make_logo_grob("mark", "dark") # mark, dark
logo <- make_logo_grob(position = "bottom-left") # left-aligned
class(logo) # "gg" "ggplot"
}
+2 -2
View File
@@ -17,6 +17,6 @@ the caption
Measure a ggplot2 object caption
}
\examples{
p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
measure_caption(p1) # Should equal 1 since no caption is required
p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
measure_caption(p1) # Should equal 1 since no caption is present
}
+6 -1
View File
@@ -4,10 +4,14 @@
\alias{na_sum}
\title{Sum a numeric that contains missing values and ignore missing values}
\usage{
na_sum(x)
na_sum(x, quiet = FALSE)
}
\arguments{
\item{x}{a numeric vector}
\item{quiet}{Logical. If \code{TRUE} (default \code{FALSE}), suppress the warning
message. Useful when calling \code{na_sum()} inside a loop or \code{dplyr} pipeline
where the message would be emitted repeatedly.}
}
\value{
the sum, ignoring any missing values
@@ -18,4 +22,5 @@ Sum a numeric that contains missing values and ignore missing values
\examples{
x <- c(2, NA, 4, 9)
na_sum(x) # 15
na_sum(x, quiet = TRUE) # 15 (no message)
}
+3 -1
View File
@@ -21,6 +21,8 @@ a rasterImage
Plot a jpeg image as a raster
}
\examples{
img <- system.file("img","civilytics_logo.jpg",package="civilytics")
\dontrun{
img <- system.file("img","Knowles_Headshot_2019_good.jpg",package="civilytics")
plot_jpeg(img)
}
}
+11
View File
@@ -0,0 +1,11 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/quarto.R
\name{quarto-helpers}
\alias{quarto-helpers}
\title{Install Civilytics Quarto Themes and Templates}
\description{
Helper functions that copy Civilytics Quarto assets from the installed
package into your Quarto project directory. Logos are stored in a single
canonical location (`inst/img/`) and copied to the paths expected by each
template at install time.
}
+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.
}
+34
View File
@@ -0,0 +1,34 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/colors.R
\name{scale_color_civilytics}
\alias{scale_color_civilytics}
\title{Civilytics color scale for ggplot2}
\usage{
scale_color_civilytics(palette = "qual", discrete = TRUE, reverse = FALSE, ...)
}
\arguments{
\item{palette}{Character. Palette name. Defaults to `"qual"`.}
\item{discrete}{Logical. `TRUE` (default) for categorical data; `FALSE`
for a continuous gradient via [ggplot2::scale_color_gradientn()].}
\item{reverse}{Logical. Reverse the palette order. Default `FALSE`.}
\item{...}{Additional arguments passed to the ggplot2 scale function.}
}
\value{
A ggplot2 scale object.
}
\description{
Applies a Civilytics brand palette to the `colour` aesthetic.
}
\examples{
library(ggplot2)
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() +
scale_color_civilytics()
ggplot(mpg, aes(displ, hwy, colour = cty)) +
geom_point() +
scale_color_civilytics("seq_navy", discrete = FALSE)
}
+34
View File
@@ -0,0 +1,34 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/colors.R
\name{scale_fill_civilytics}
\alias{scale_fill_civilytics}
\title{Civilytics fill scale for ggplot2}
\usage{
scale_fill_civilytics(palette = "qual", discrete = TRUE, reverse = FALSE, ...)
}
\arguments{
\item{palette}{Character. Palette name. Defaults to `"qual"`.}
\item{discrete}{Logical. `TRUE` (default) for categorical data; `FALSE`
for a continuous gradient via [ggplot2::scale_fill_gradientn()].}
\item{reverse}{Logical. Reverse the palette order. Default `FALSE`.}
\item{...}{Additional arguments passed to the ggplot2 scale function.}
}
\value{
A ggplot2 scale object.
}
\description{
Applies a Civilytics brand palette to the `fill` aesthetic.
}
\examples{
library(ggplot2)
ggplot(mpg, aes(class, fill = class)) +
geom_bar() +
scale_fill_civilytics()
ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
geom_tile() +
scale_fill_civilytics("seq_ember", discrete = FALSE)
}
+56
View File
@@ -0,0 +1,56 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/logo.R
\name{stamp_logo_png}
\alias{stamp_logo_png}
\title{Stamp the Civilytics logo onto a saved raster (PNG) image}
\usage{
stamp_logo_png(
path,
type = c("wordmark", "mark"),
variant = c("light", "dark"),
position = c("bottom-right", "bottom-left", "top-right", "top-left"),
width_frac = 0.15,
margin_frac = 0.02
)
}
\arguments{
\item{path}{Character. Path to the PNG to stamp. The file is overwritten
in place at its original pixel dimensions.}
\item{type}{Character. `"wordmark"` (default) or `"mark"`. As in
[make_logo_grob()].}
\item{variant}{Character. `"light"` (default, dark logo for light
backgrounds) or `"dark"` (reverse logo for dark backgrounds).}
\item{position}{Character. Corner placement: `"bottom-right"` (default),
`"bottom-left"`, `"top-right"`, or `"top-left"`.}
\item{width_frac}{Numeric. Logo width as a fraction of the image width
(default `0.15`). Height follows from the logo's aspect ratio.}
\item{margin_frac}{Numeric. Padding from the edges as a fraction of the
image width (default `0.02`).}
}
\value{
`path`, invisibly.
}
\description{
The raster analogue of [civilytics_logo()] for outputs that are already
rendered to a file rather than held as a ggplot/grob — e.g. a `flextable`
exported to PNG, or any `grDevices::png()` / `ragg::agg_png()` output.
Resolves the *same* brand asset that [make_logo_grob()] uses, so file-based
tables stay visually consistent with logo-branded plots, and composites it
into a corner of the image. Pure base-graphics + grid + png — no new
package dependencies.
}
\examples{
\dontrun{
# Brand a table exported to PNG so it matches civilytics_logo()-branded plots
ragg::agg_png("table.png", width = 8, height = 4, units = "in", res = 200)
plot(flextable::flextable(head(mtcars)))
dev.off()
stamp_logo_png("table.png") # wordmark, bottom-right
stamp_logo_png("table.png", type = "mark", position = "bottom-left")
}
}
+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.
}
+106 -11
View File
@@ -2,33 +2,128 @@
% Please edit documentation in R/theme.R
\name{theme_civilytics}
\alias{theme_civilytics}
\title{Make the Civilytics plot theme}
\title{Civilytics ggplot2 theme}
\usage{
theme_civilytics(
font_size = 14,
font_family = "",
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
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"]),
grid = c("y", "x", "both", "none"),
paper_bg = TRUE
)
}
\arguments{
\item{font_size}{default 14, a number representing the base font for the theme}
\item{font_size}{Numeric. Base font size in points. Default `14`.}
\item{font_family}{default "", a character for the font family to use in the theme}
\item{font_family}{Character. Font family for body/axis text. Default
`"Inter"` (loaded via showtext).}
\item{line_size}{default 0.5, the line size to use for the theme}
\item{title_family}{Character. Font family for plot titles and strip labels.
Default `"Libre Franklin"` (loaded via showtext).}
\item{rel_small}{default 12/14, the scale factor to create a small font from the base font_size}
\item{line_size}{Numeric. Base line width. Default `0.5`.}
\item{rel_tiny}{default 11/14, the scale factor to create a tiny font from the base font_size}
\item{rel_small}{Numeric. Scale factor for small text relative to
`font_size`. Default `12/14`.}
\item{rel_large}{default 16/14, the scale factor to create a large font from the base font_size}
\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{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 ggplot2 theme object suitable for combining with ggplot objects to theme them
A complete ggplot2 [ggplot2::theme()] object.
}
\description{
Make the Civilytics plot theme
A complete ggplot2 theme built on [ggplot2::theme_grey()] using the
Civilytics brand color palette and typography. Requires ggplot2 >= 4.0.0
for the `ink`, `paper`, and `accent` base-theme parameters.
}
\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
automatically via [showtext] when the package is attached. Call
[civilytics_load_fonts()] to reload them if needed.
}
\section{Font size hierarchy}{
All text sizes are derived from `font_size` using relative scale factors.
At the default `font_size = 14`:
| Element | Scale factor | Default size |
|:--------|:-------------|:-------------|
| Plot title | `rel_large` (1.43x) | ~20 pt |
| Subtitle | 1.0x | 14 pt |
| Axis text (tick labels) | `rel_small` (0.86x) | ~12 pt |
| Axis titles | `rel_small` (0.86x) | ~12 pt |
| Legend text | `rel_small` (0.86x) | ~12 pt |
| Caption | `rel_tiny` (0.79x) | ~11 pt |
| Legend title | `rel_tiny` (0.79x) | ~11 pt |
| Strip text (facets) | `rel_small` (0.86x) | ~12 pt |
To uniformly scale all text, change `font_size`. To adjust only the title
prominence, change `rel_large`. When using [civilytics_logo()] to add a
logo below the plot, pass `font_scale` to compensate for viewport
shrinkage.
}
\examples{
\dontrun{
library(ggplot2)
# Default editorial theme
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics()
# With both gridlines and brand colors
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() +
scale_color_civilytics() +
theme_civilytics(grid = "both")
# Transparent background for embedding
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics(paper_bg = FALSE)
# Larger text for poster or display
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_civilytics(font_size = 18)
}
}
+83
View File
@@ -0,0 +1,83 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/theme.R
\name{theme_civilytics_dark}
\alias{theme_civilytics_dark}
\title{Dark variant of the Civilytics ggplot2 theme}
\usage{
theme_civilytics_dark(
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"]),
grid = c("y", "x", "both", "none"),
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{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{
Convenience wrapper around [theme_civilytics()] with dark-background
defaults: navy (`#1A2E4A`) paper, warm off-white (`#FAF7F2`) ink, and
a lighter ember accent (`#E07840`). Facet strips use primary navy.
}
\details{
Pair with `make_logo_grob(variant = "dark")` for the white logo.
}
\examples{
\dontrun{
library(ggplot2)
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() +
scale_color_civilytics() +
theme_civilytics_dark()
}
}
+76
View File
@@ -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()
}
}
+78
View File
@@ -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()
}
}
+80
View File
@@ -0,0 +1,80 @@
% 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 = 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"]),
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 `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{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()
}
}
+76
View File
@@ -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()
}
}
+26
View File
@@ -0,0 +1,26 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/quarto.R
\name{use_civilytics_brand}
\alias{use_civilytics_brand}
\title{Install the Civilytics brand file only}
\usage{
use_civilytics_brand(path = ".", force = FALSE)
}
\arguments{
\item{path}{Character. Project directory to install into. Default `"."`.}
\item{force}{Logical. Overwrite existing files? Default `FALSE`.}
}
\value{
Invisible `NULL`.
}
\description{
Copies `_brand.yml` and the logo assets it references into your
Quarto project. This gives you Quarto 1.5+ automatic brand
styling (colors, fonts, logos) without the full theme SCSS.
}
\examples{
\dontrun{
use_civilytics_brand()
}
}
+27
View File
@@ -0,0 +1,27 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/quarto.R
\name{use_civilytics_revealjs}
\alias{use_civilytics_revealjs}
\title{Install the Civilytics Reveal.js extension}
\usage{
use_civilytics_revealjs(path = ".", force = FALSE)
}
\arguments{
\item{path}{Character. Project directory to install into. Default `"."`.}
\item{force}{Logical. Overwrite existing files? Default `FALSE`.}
}
\value{
Invisible `NULL`.
}
\description{
Copies the Civilytics Reveal.js Quarto extension into
`_extensions/civilytics-reveal/` under `path`, including brand logos
from the package. After running this function, add
`format: civilytics-reveal-revealjs` to your `.qmd` YAML front-matter.
}
\examples{
\dontrun{
use_civilytics_revealjs()
}
}
+26
View File
@@ -0,0 +1,26 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/quarto.R
\name{use_civilytics_theme}
\alias{use_civilytics_theme}
\title{Install the Civilytics document theme}
\usage{
use_civilytics_theme(path = ".", force = FALSE)
}
\arguments{
\item{path}{Character. Project directory to install into. Default `"."`.}
\item{force}{Logical. Overwrite existing files? Default `FALSE`.}
}
\value{
Invisible `NULL`.
}
\description{
Copies Civilytics HTML, PDF (LaTeX), and Typst theme files into
your Quarto project. Also installs `_brand.yml` and the logo
assets it references.
}
\examples{
\dontrun{
use_civilytics_theme()
}
}
+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])
})

Some files were not shown because too many files have changed in this diff Show More