Author SHA1 Message Date
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
jared f3f13fcfe5 fix message unicode
Gitea Organization/civilyticsR/pipeline/head This commit looks good
2024-09-19 16:14:30 -04:00
jared 962f8ea6b7 add rounding utilities
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2024-09-19 14:32:43 -04:00
jared f4a4ee85a7 fix doco
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
Gitea Organization/civilyticsR/pipeline/pr-master There was a failure building this commit
2024-08-09 15:34:39 -04:00
jared 82b8db7f84 fix proportion ci calc and theme
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2024-08-09 15:05:59 -04:00
Jared Knowles 4f49ae6d5a dockerfile edits
Gitea Organization/civilyticsR/pipeline/head This commit looks good
2023-07-20 15:44:17 -04:00
Jared Knowles e27ff481c1 fix docker build agent to have correct geo dependencies installed
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2023-07-20 15:08:16 -04:00
Jared Knowles 91dceb9c1c somehow licenses are important
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2023-07-20 14:57:08 -04:00
Jared Knowles c6079eb515 working build
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2023-07-20 14:51:04 -04:00
Jared Knowles 0ad87eb333 fix dockerfile
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2023-07-20 14:18:02 -04:00
Jared Knowles ac63932a31 add additional functions
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2023-07-20 11:48:42 -04:00
Jared Knowles 9acebb5133 unit test match funs
Gitea Organization/civilyticsR/pipeline/head This commit looks good
2023-07-19 17:37:18 -04:00
Jared Knowles a0b4abdb9e add code coverage
Gitea Organization/civilyticsR/pipeline/head This commit looks good
2022-10-31 12:39:13 -04:00
Jared Knowles f0133e32b7 typo
Gitea Organization/civilyticsR/pipeline/head This commit looks good
2022-10-16 19:31:42 -04:00
Jared Knowles 91dad76526 cleanup tests and build
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 19:30:05 -04:00
Jared Knowles a02e72e618 get dockerfile requirements right
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 19:20:20 -04:00
Jared Knowles 4a2cd9e1dd refresh doco
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 19:17:38 -04:00
Jared Knowles f50290ba40 nest stages
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 19:15:33 -04:00
Jared Knowles 8b8dd50de3 fill out testing
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 19:08:06 -04:00
Jared Knowles 34cc4a5c1a update dockerfile
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 18:59:48 -04:00
Jared Knowles 6f4ccc0d4e convert makefile to jenkinsfile
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 18:58:22 -04:00
Jared Knowles 920098e18d remove uneeded git dependency
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 18:45:37 -04:00
Jared Knowles c004628682 typo
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 18:42:09 -04:00
Jared Knowles f2dca18041 typo
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 18:41:14 -04:00
Jared Knowles 72afbaeefa dockerfile version
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 18:38:10 -04:00
Jared Knowles d061c4e802 configure jenkinsfile better
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 18:19:39 -04:00
Jared Knowles 904ca1dbc8 typos
Gitea Organization/civilyticsR/pipeline/head Something is wrong with the build of this commit
2022-10-16 18:08:53 -04:00
Jared Knowles 1550c2cb5b rockstar test
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 18:07:42 -04:00
Jared Knowles b44a254029 fix jenkinsfile parse
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 17:58:08 -04:00
Jared Knowles 399cb7101d make file test
Gitea Organization/civilyticsR/pipeline/head There was a failure building this commit
2022-10-16 17:56:27 -04:00
122 changed files with 8563 additions and 1237 deletions
+12 -4
View File
@@ -1,4 +1,12 @@
^.*\.Rproj$ ^.*\.Rproj$
^\.Rproj\.user$ ^\.Rproj\.user$
^\.github$ ^\.github$
^README\.Rmd$ ^\.gitea$
^README\.Rmd$
^Makefile$
^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}
+37 -24
View File
@@ -1,24 +1,37 @@
Package: civilytics Package: civilytics
Type: Package Type: Package
Title: Utilities Functions for Civilytics Title: Brand Themes, Color Palettes, and Utility Functions for Civilytics
Version: 0.1.0 Version: 0.2.0
Author: Jared E. Knowles <jared@civilytics.com> Authors@R:
Maintainer: Jared E. Knowles <jared@civilytics.com> person("Jared", "E. Knowles", email = "jared@civilytics.com",
Description: House R functions for Civilytics Consulting LLC role = c("aut", "cre"))
This package implements a variety of useful functions for creating and Description: Provides a complete ggplot2 brand theme system for Civilytics
branding analyses produced by Civilytics Consulting LLC. Consulting LLC, including editorial light, dark, and slide-optimized themes;
License: LICENSE 10 curated color palettes for qualitative, sequential, and diverging data;
Depends: logo composition utilities; Quarto themes for HTML reports, PDF, Typst, and
R (>= 2.15.1) Reveal.js presentations; and data-wrangling helpers for public-sector
Imports: analysis.
ggplot2, License: LGPL (>= 3)
jpeg, URL: https://gitea.civilytics.org/Civilytics/civilyticsR
png, BugReports: https://gitea.civilytics.org/Civilytics/civilyticsR/issues
stringr, Depends:
gridExtra, R (>= 4.1.0)
grid Imports:
Encoding: UTF-8 ggplot2 (>= 4.0.0),
LazyData: true jpeg,
Suggests: jsonlite,
testthat png,
RoxygenNote: 7.1.1 stringr,
gridExtra,
grid,
stringdist,
showtext,
sysfonts
Encoding: UTF-8
Suggests:
testthat (>= 3.0.0),
tidycensus,
quarto
Config/testthat/edition: 3
Config/roxygen2/version: 8.0.0
RoxygenNote: 7.3.3
+27
View File
@@ -0,0 +1,27 @@
# [Choice] R version: 4, 4.2, 4.1, 4.0
ARG VARIANT=4.2
# [Choice] Base image. Minimal (r-ver), tidyverse installed (tidyverse), or full image (binder): rocker/r-ver, rocker/tidyverse, rocker/binder
ARG BASE_IMAGE=rocker/r-ver
FROM ${BASE_IMAGE}:${VARIANT}
RUN apt-get update && apt-get install -y --no-install-recommends \
sudo \
libcurl4-gnutls-dev \
libxml2-dev \
libcairo2-dev \
libxt-dev \
libjpeg-dev \
libpng-dev \
openjdk-11-jdk \
libssl-dev \
libssh2-1-dev \
libudunits2-dev \
libgdal-dev \
libgeos-dev \
libproj-dev \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/lib/shiny-server/bookmarks/shiny
RUN install2.r ggplot2 jpeg png stringr gridExtra grid testthat covr tidycensus stringdist
+163
View File
@@ -0,0 +1,163 @@
GNU Lesser General Public License
=================================
_Version 3, 29 June 2007_
_Copyright © 2007 Free Software Foundation, Inc. &lt;<http://fsf.org/>&gt;_
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
### 0. Additional Definitions
As used herein, “this License” refers to version 3 of the GNU Lesser
General Public License, and the “GNU GPL” refers to version 3 of the GNU
General Public License.
“The Library” refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An “Application” is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A “Combined Work” is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the “Linked
Version”.
The “Minimal Corresponding Source” for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The “Corresponding Application Code” for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
### 1. Exception to Section 3 of the GNU GPL
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
### 2. Conveying Modified Versions
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
* **a)** under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
* **b)** under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
### 3. Object Code Incorporating Material from Library Header Files
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
* **a)** Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
* **b)** Accompany the object code with a copy of the GNU GPL and this license
document.
### 4. Combined Works
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
* **a)** Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
* **b)** Accompany the Combined Work with a copy of the GNU GPL and this license
document.
* **c)** For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
* **d)** Do one of the following:
- **0)** Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
- **1)** Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that **(a)** uses at run time
a copy of the Library already present on the user's computer
system, and **(b)** will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
* **e)** Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option **4d0**, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option **4d1**, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
### 5. Combined Libraries
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
* **a)** Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
* **b)** Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
### 6. Revised Versions of the GNU Lesser General Public License
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License “or any later version”
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
+26
View File
@@ -0,0 +1,26 @@
# h/t to @jimhester and @yihui for this parse block:
# https://github.com/yihui/knitr/blob/dc5ead7bcfc0ebd2789fe99c527c7d91afb3de4a/Makefile#L1-L4
# Note the portability change as suggested in the manual:
# https://cran.r-project.org/doc/manuals/r-release/R-exts.html#Writing-portable-packages
PKGNAME = `sed -n "s/Package: *\([^ ]*\)/\1/p" DESCRIPTION`
PKGVERS = `sed -n "s/Version: *\([^ ]*\)/\1/p" DESCRIPTION`
all: check
build: install_deps
R CMD build .
check: build
R CMD check --no-manual $(PKGNAME)_$(PKGVERS).tar.gz
install_deps:
Rscript \
-e 'if (!requireNamespace("remotes")) install.packages("remotes")' \
-e 'remotes::install_deps(dependencies = TRUE)'
install: build
R CMD INSTALL $(PKGNAME)_$(PKGVERS).tar.gz
clean:
@rm -rf $(PKGNAME)_$(PKGVERS).tar.gz $(PKGNAME).Rcheck
+75 -34
View File
@@ -1,34 +1,75 @@
# Generated by roxygen2: do not edit by hand # Generated by roxygen2: do not edit by hand
export(add_logo) export(beep)
export(add_logo_ga) export(add_logo)
export(countCleanr) export(add_logo_ga)
export(countDots) export(agresti_coull_interval)
export(countNA) export(civilytics_colors)
export(dbSafeNames) export(civilytics_load_fonts)
export(findDots) export(civilytics_logo)
export(get_png) export(civilytics_pal)
export(grade_level_to_num) export(civilytics_palette)
export(has_caption) export(civilytics_palettes)
export(make_logo_grob) export(clopper_pearson)
export(measure_caption) export(countCleanr)
export(na_zero) export(countDots)
export(nvals) export(countNA)
export(plot_jpeg) export(dbSafeNames)
export(pretty_count) export(findDots)
export(pretty_per) export(get_fips)
export(race_short_names) export(get_png)
export(safe_max) export(get_stabbr)
export(simpleCap) export(grade_level_to_num)
export(star_subs) export(has_caption)
export(theme_civilytics) export(make_logo_grob)
import(ggplot2) export(match_test)
importFrom(ggplot2,theme) export(measure_caption)
importFrom(graphics,plot) export(na_sum)
importFrom(graphics,rasterImage) export(na_zero)
importFrom(grid,grid.draw) export(nvals)
importFrom(grid,rasterGrob) export(outersect)
importFrom(gridExtra,arrangeGrob) export(perturb_count)
importFrom(jpeg,readJPEG) export(plot_jpeg)
importFrom(png,readPNG) export(postcode_lookup)
importFrom(stringr,str_count) export(pretty_count)
export(pretty_per)
export(race_short_names)
export(random_round)
export(rnh)
export(round_to_nearest_half)
export(safe_max)
export(safe_ratio)
export(scale_color_civilytics)
export(scale_fill_civilytics)
export(simpleCap)
export(star_subs)
export(theme_civilytics)
export(theme_civilytics_dark)
export(theme_civilytics_dark_map)
export(theme_civilytics_map)
export(theme_civilytics_slide)
export(theme_civilytics_slide_map)
export(trim_max)
export(use_civilytics_brand)
export(use_civilytics_revealjs)
export(use_civilytics_theme)
export(waldInterval)
export(z_gap_test)
export(z_univariate)
import(ggplot2)
importFrom(ggplot2,annotation_custom)
importFrom(ggplot2,ggplot)
importFrom(ggplot2,theme)
importFrom(ggplot2,theme_void)
importFrom(graphics,rasterImage)
importFrom(grid,grid.draw)
importFrom(grid,rasterGrob)
importFrom(gridExtra,arrangeGrob)
importFrom(jpeg,readJPEG)
importFrom(png,readPNG)
importFrom(stats,qbeta)
importFrom(stats,qnorm)
importFrom(stats,runif)
importFrom(stringdist,stringsim)
importFrom(stringr,str_count)
importFrom(utils,flush.console)
+40
View File
@@ -0,0 +1,40 @@
#' @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
"_PACKAGE"
## usethis namespace: start
## usethis namespace: end
NULL
# state.abb and state.name are lazy data from the datasets package (base R).
# They cannot be imported via @importFrom -- suppress the R CMD check NOTE here.
utils::globalVariables(c("state.abb", "state.name"))
+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),
...
)
}
}
+1 -6
View File
@@ -37,8 +37,6 @@ countCleanr <- function(x){
#' #'
#' @return the names of columns in the dataframe with one or more entries equal to "." #' @return the names of columns in the dataframe with one or more entries equal to "."
#' @export #' @export
#'
#' @examples
findDots <- function(data){ findDots <- function(data){
return(names(data)[lapply(data, countDots) > 0]) return(names(data)[lapply(data, countDots) > 0])
@@ -49,9 +47,8 @@ findDots <- function(data){
#' @param x a character vector #' @param x a character vector
#' #'
#' @return #' @return
#' An integer counting the number of "." occurences in a vector
#' @export #' @export
#'
#' @examples
countDots <- function(x){ countDots <- function(x){
len <- length(x[x == "." & !is.na(x)]) len <- length(x[x == "." & !is.na(x)])
totlen <- length(x) totlen <- length(x)
@@ -65,8 +62,6 @@ countDots <- function(x){
#' #'
#' @return a vector the same length as the input vector with clean names #' @return a vector the same length as the input vector with clean names
#' @export #' @export
#'
#' @examples
dbSafeNames <- function(names) { dbSafeNames <- function(names) {
names = gsub('[^a-z0-9]+','_',tolower(names)) names = gsub('[^a-z0-9]+','_',tolower(names))
names = make.names(names, unique = TRUE, allow_ = TRUE) names = make.names(names, unique = TRUE, allow_ = TRUE)
+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."
)
}
}
+48
View File
@@ -0,0 +1,48 @@
# Join utilities
#' Test the join between two sets of identifiers
#'
#' @param x a vector of identifiers to check against y
#' @param y a vector of identifiers to check against x
#' @param distinct logical, should duplicate values of x and y be removed before testing
#'
#' @return nothing, print a summary of match statistics to the console
#' @export
#'
#' @examples
#' x <- LETTERS
#' y <- c(letters, LETTERS)
#' match_test(x, y)
match_test <- function(x, y, distinct = TRUE) {
if (distinct) {
x <- unique(x)
y <- unique(y)
cat("**** Distinct Matches ****")
cat("\n")
}
# TODO: DO not report 100% if there is even 1 mismatch
xiny <- sum(x %in% y)
total_x <- length(x)
yinx <- sum(y %in% x)
total_y <- length(y)
cat("**** Match Summary ****")
cat("\n")
cat("X in Y")
cat("\n")
cat(paste0("Of the ", total_x, " X values, ", xiny, " (",
100*round(xiny/total_x, 2), "%) were matched."))
cat("\n")
cat("********************************************")
cat("\n")
cat("Y in X")
cat("\n")
cat(paste0("Of the ", total_y, " Y values, ", yinx, " (",
100*round(yinx/total_y, 2), "%) were matched."))
cat("\n")
cat("******************************************")
}
+231 -46
View File
@@ -9,11 +9,13 @@
#' @importFrom jpeg readJPEG #' @importFrom jpeg readJPEG
#' @importFrom graphics rasterImage #' @importFrom graphics rasterImage
#' @examples #' @examples
#' img <- system.file("img","Civilytics Consulting Logo.jpg",package="civilytics") #' \dontrun{
#' img <- system.file("img","Knowles_Headshot_2019_good.jpg",package="civilytics")
#' plot_jpeg(img) #' plot_jpeg(img)
#' }
plot_jpeg <- function(path, add=FALSE, upscale = TRUE) 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] res = dim(jpg)[2:1] # get the resolution, [x, y]
if (upscale){ if (upscale){
res <- res * 3 res <- res * 3
@@ -30,13 +32,10 @@ plot_jpeg <- function(path, add=FALSE, upscale = TRUE)
#' #'
#' @param filename a character with file path to a png file #' @param filename a character with file path to a png file
#' #'
#' @return #' @return a plotted rasteGrob of a png image
#' @export #' @export
#' @importFrom png readPNG #' @importFrom png readPNG
#' @importFrom grid rasterGrob #' @importFrom grid rasterGrob
#'
#' @examples
#'
get_png <- function(filename) { get_png <- function(filename) {
grid::rasterGrob(png::readPNG(filename), interpolate = TRUE) grid::rasterGrob(png::readPNG(filename), interpolate = TRUE)
} }
@@ -47,31 +46,73 @@ get_png <- function(filename) {
#' @param plot a ggplot2 grob #' @param plot a ggplot2 grob
#' @param logo a logo grob created by make_logo_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 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 #' @return a grob with a logo attached to it ready to plot
#' @importFrom ggplot2 theme #' @importFrom ggplot2 theme
#' @importFrom gridExtra arrangeGrob #' @importFrom gridExtra arrangeGrob
#' @export #' @export
#' add_logo <- function(plot, logo, margin_param = NULL, font_scale = 1.1,
#' @examples position = c("bottom-right", "bottom-left",
add_logo <- function(plot, logo, margin_param = NULL) { "top-right", "top-left")) {
if(has_caption(plot)) { position <- match.arg(position)
# convert the caption size to a negative number and on the "pt" scale at_top <- grepl("top", position)
# p1$theme$plot.caption$size * 1.1
# Count the number of lines, which is this + 1 # Capture the plot's background color so we can fill the entire composed
# For each line, we can add a certain negative space to align the logo # grob with it. The logo grob must stay transparent (theme_void) so that
cap_lines <- measure_caption(plot) # caption/axis text overlapping via negative margins remains visible.
if (!is.null(margin_param)) { bg_fill <- plot$theme$plot.background$fill
plot <- plot + theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt"))
} else { # Inflate text sizes to compensate for arrangeGrob viewport shrinkage.
plot <- plot + theme(plot.margin = unit(c(7, 7, cap_lines * -52, 7), "pt")) # All theme text elements use rel() sizing, so scaling the root 'text'
} # element cascades to titles, axis labels, legends, captions, and strips.
} else { if (!is.null(font_scale) && font_scale != 1) {
plot <- plot + theme(plot.margin = unit(c(7, 7, -14, 7), "pt")) 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), if (at_top) {
padding = unit(0.1, "line")) # 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 #' Measure a ggplot2 object caption
@@ -84,8 +125,8 @@ add_logo <- function(plot, logo, margin_param = NULL) {
#' @export #' @export
#' #'
#' @examples #' @examples
#' p1 <- qplot(mpg, wt, data = mtcars) #' p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
#' measure_caption(p1) # Should equal 1 since no caption is required #' measure_caption(p1) # Should equal 1 since no caption is present
measure_caption <- function(gg) { measure_caption <- function(gg) {
if (has_caption(gg)) { if (has_caption(gg)) {
stringr::str_count(gg$labels$caption, pattern = "\n") + 1 stringr::str_count(gg$labels$caption, pattern = "\n") + 1
@@ -104,21 +145,23 @@ measure_caption <- function(gg) {
#' @export #' @export
#' #'
#' @examples #' @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(p1) # FALSE
has_caption <- function(gg) { has_caption <- function(gg) {
any(names(gg$labels) == "caption") 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 plot_list a list containing ggplot2 objects
#' @param logo a grob containing the logo created with `make_logo_grob` #' @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 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 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 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 #' @return a grid object
#' @note The resulting object needs to be drawn to the screen using grid.draw() #' @note The resulting object needs to be drawn to the screen using grid.draw()
#' @importFrom gridExtra arrangeGrob #' @importFrom gridExtra arrangeGrob
#' @importFrom ggplot2 theme #' @importFrom ggplot2 theme
@@ -126,18 +169,34 @@ has_caption <- function(gg) {
#' @export #' @export
#' #'
#' @examples #' @examples
#' \dontrun{
#' library(ggplot2); library(grid) #' library(ggplot2); library(grid)
#' tmp_plot <- ggplot(mtcars) + aes(x = hp, y = disp) + geom_point() + theme_civilytics() #' tmp_plot <- ggplot(mtcars) + aes(x = hp, y = disp) + geom_point() + theme_civilytics()
#' tmp_logo <- make_logo_grob() #' tmp_logo <- make_logo_grob()
#' plot_and_logo <- add_logo(tmp_plot, tmp_logo) #' plot_and_logo <- add_logo(tmp_plot, tmp_logo)
#' grid.draw(plot_and_logo) #' grid.draw(plot_and_logo)
#' dev.off() #' 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 # Change position of logo depending on if plot has a caption
if (!is.null(margin_param)) { if (!is.null(margin_param)) {
margin <- theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt")) margin <- theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt"))
} else if (any(unlist(lapply(plot_list, has_caption)))) { } 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")) margin <- theme(plot.margin = unit(c(7, 7, -7 * sqrt(cap_lines), 7), "pt"))
} else { } else {
margin <- theme(plot.margin = unit(c(7, 7, 7, 7), "pt")) margin <- theme(plot.margin = unit(c(7, 7, 7, 7), "pt"))
@@ -150,29 +209,155 @@ add_logo_ga <- function(plot_list, logo, nrow = 1, widths = NULL, margin_param =
} }
if (nrow != 1) { 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 { } 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 #' Get a Civilytics Logo grob
#' #'
#' @return a gg object which contains the logo file stored as a Grob suitable for manipulating in #' Returns a ggplot object containing the Civilytics logo as a rasterGrob,
#' grid #' 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 #' @export
#' @import ggplot2 #' @importFrom ggplot2 ggplot theme_void annotation_custom
#' @examples #' @examples
#' logo <- make_logo_grob() #' logo <- make_logo_grob() # wordmark, light
#' class(logo) # gg #' logo <- make_logo_grob("mark", "dark") # mark, dark
make_logo_grob <- function() { #' logo <- make_logo_grob(position = "bottom-left") # left-aligned
logo_grob <- ggplot(mapping = aes(x = 0:1, y = 1)) + #' class(logo) # "gg" "ggplot"
theme_void() + make_logo_grob <- function(type = c("wordmark", "mark"),
annotation_custom(get_png(system.file("img", "civilytics_logo.png", variant = c("light", "dark"),
package="civilytics")), xmin= 0.7, xmax = 1) position = c("bottom-right", "bottom-left",
logo_grob "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)
} }
+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) != ""
}
+92
View File
@@ -0,0 +1,92 @@
# https://github.com/cran/binom/blob/master/R/binom.confint.R
# Consider importing and crediting this code ^^
# https://towardsdatascience.com/five-confidence-intervals-for-proportions-that-you-should-know-about-7ff5484c024f
# https://andrewpwheeler.com/2020/11/30/confidence-intervals-around-proportions/
#' Get a simple Clopper Pearson interval
#'
#' @param num number of successes
#' @param den number of trials
#' @param conf.level default 0.95, set the confidence interval to return
#'
#' @return three values forming the upper and lower bounds of the confidence region and the true value
#' @export
clopper_pearson <- function(num, den, conf.level = 0.95) {
# Same results as binom.test in base R
quant <- (1 - conf.level) / 2
low <- qbeta(quant, num, den-num+1)
hi <- qbeta(1-quant, num+1, den-num)
obs <- num/den
return(c("low" = low, "observed" = obs,"high" = hi))
}
# z_gap_test_v <- Vectorize(z_gap_test,
# SIMPLIFY = TRUE)# we only want to return a scalar
#z_gap_test(a_prop = 0.051, a_count = 2000, b_prop = 0.11, b_count = 100)
#' Calculate a univariate z score by comparing to a population
#'
#' @param unit_prop proportion for the group we are comparing
#' @param global_prop the global proportion
#' @param unit_denom the population size for the group we are comparing
#'
#' @return a z-score
#' @export
#'
#' @examples
#' z_univariate(unit_prop = 0.13, global_prop = 0.11, unit_denom = 2500)
z_univariate <- function(unit_prop, global_prop, unit_denom) {
num <- unit_prop - global_prop
denom <- sqrt(
(global_prop * (1-global_prop))/unit_denom
)
z = num / denom
return(z)
}
#' Calculate a Wald interval
#'
#' @param x the numerator, number of times the event occurs
#' @param n the denominator, the number of trials
#' @param conf.level default 0.95, set the confidence interval to return
#'
#' @return two values forming the upper and lower bounds of the confidence region
#' @export
#'
#' @examples
#' waldInterval(x = 20, n =40) #this will return 0.345 and 0.655
waldInterval <- function(x, n, conf.level = 0.95){
p <- x/n
sd <- sqrt(p*((1-p)/n))
z <- qnorm(c( (1 - conf.level)/2, 1 - (1-conf.level)/2)) #returns the value of thresholds at which conf.level has to be cut at. for 95% CI, this is -1.96 and +1.96
ci <- p + z*sd
names(ci) <- c('lwr', 'upr')
return(ci)
}
#' Calculate the Agresti-Coull interval
#'
#' @param num number of successes
#' @param den number of trials
#' @param conf.level default 0.95, confidence level for the interval
#'
#' @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))
}
+193
View File
@@ -0,0 +1,193 @@
#' 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
.copy_pkg_file(
"quarto/typst/civilytics-typst.typ",
"typst/civilytics-typst.typ",
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: typst/civilytics-typst.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)
}
+622 -168
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 #' A complete ggplot2 theme built on [ggplot2::theme_grey()] using the
#' @param font_family default "", a character for the font family to use in the theme #' Civilytics brand color palette and typography. Requires ggplot2 >= 4.0.0
#' @param line_size default 0.5, the line size to use for the theme #' for the `ink`, `paper`, and `accent` base-theme parameters.
#' @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 #' Produces an editorial, Pew-style layout: visible x-axis line to ground
#' @param rel_large default 16/14, the scale factor to create a large font from the base font_size #' the data, light horizontal gridlines for reference, no panel border or
#' @importFrom graphics plot #' y-axis line. Plot title and caption are left-aligned to the full plot
#' @return a ggplot2 theme object suitable for combining with ggplot objects to theme them #' region.
#' @export #'
theme_civilytics <- #' Brand fonts (Inter for UI text, Libre Franklin for titles) are loaded
function (font_size = 14, #' automatically via [showtext] when the package is attached. Call
font_family = "", #' [civilytics_load_fonts()] to reload them if needed.
line_size = 0.5, #'
rel_small = 12 / 14, #' @param font_size Numeric. Base font size in points. Default `14`.
rel_tiny = 11 / 14, #' @param font_family Character. Font family for body/axis text. Default
rel_large = 16 / 14) { #' `"Inter"` (loaded via showtext).
half_line <- font_size / 2 #' @param title_family Character. Font family for plot titles and strip labels.
small_size <- rel_small * font_size #' Default `"Libre Franklin"` (loaded via showtext).
theme_grey(base_size = font_size, base_family = font_family) %+replace% #' @param line_size Numeric. Base line width. Default `0.5`.
theme( #' @param rel_small Numeric. Scale factor for small text relative to
line = element_line( #' `font_size`. Default `12/14`.
color = "black", #' @param rel_tiny Numeric. Scale factor for tiny text relative to `font_size`.
size = line_size, #' Default `11/14`.
linetype = 1, #' @param rel_large Numeric. Scale factor for large text (titles) relative to
lineend = "butt" #' `font_size`. Default `20/14` (~1.43x), matching the Civilytics editorial
), #' design system.
rect = element_rect( #' @param ink Character. Hex code for foreground/text color. Defaults to
fill = NA, #' [civilytics_colors]`["ink"]` (`#0E1A2B`).
color = NA, #' @param paper Character. Hex code for background color. Defaults to
size = line_size, #' [civilytics_colors]`["paper"]` (`#FAF7F2`).
linetype = 1 #' @param accent Character. Hex code for accent/highlight color. Defaults to
), #' [civilytics_colors]`["ember_600"]` (`#C25311`).
text = element_text( #' @param strip_color Character. Hex code for facet strip background. Defaults
family = font_family, #' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).
face = "plain", #' @param grid Character. Which major gridlines to draw: `"y"` (default,
color = "black", #' horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.
size = font_size, #' @param paper_bg Logical. If `TRUE` (default), fill the plot and panel
hjust = 0.5, #' backgrounds with the warm `paper` color. Set to `FALSE` for a
vjust = 0.5, #' transparent background (useful for slides or overlay on colored
angle = 0, #' surfaces).
lineheight = 0.9, #'
margin = margin(), #' @section Font size hierarchy:
debug = FALSE #' All text sizes are derived from `font_size` using relative scale factors.
), #' At the default `font_size = 14`:
axis.line = element_line( #'
color = "black", #' | Element | Scale factor | Default size |
size = line_size, #' |:--------|:-------------|:-------------|
lineend = "square" #' | Plot title | `rel_large` (1.43x) | ~20 pt |
), #' | Subtitle | 1.0x | 14 pt |
axis.line.x = NULL, #' | Axis text (tick labels) | `rel_small` (0.86x) | ~12 pt |
axis.line.y = NULL, #' | Axis titles | `rel_small` (0.86x) | ~12 pt |
axis.text = element_text(color = "black", #' | Legend text | `rel_small` (0.86x) | ~12 pt |
size = small_size), #' | Caption | `rel_tiny` (0.79x) | ~11 pt |
axis.text.x = element_text(margin = margin(t = small_size / 4), #' | Legend title | `rel_tiny` (0.79x) | ~11 pt |
vjust = 1), #' | Strip text (facets) | `rel_small` (0.86x) | ~12 pt |
axis.text.x.top = element_text(margin = margin(b = small_size / 4), #'
vjust = 0), #' To uniformly scale all text, change `font_size`. To adjust only the title
axis.text.y = element_text(margin = margin(r = small_size / 4), #' prominence, change `rel_large`. When using [civilytics_logo()] to add a
hjust = 1), #' logo below the plot, pass `font_scale` to compensate for viewport
axis.text.y.right = element_text(margin = margin(l = small_size / 4), #' shrinkage.
hjust = 0), #'
axis.ticks = element_line(color = "black", #' @return A complete ggplot2 [ggplot2::theme()] object.
size = line_size), #' @export
axis.ticks.length = unit(half_line / 2, #'
"pt"), #' @examples
axis.title.x = element_text(margin = margin(t = half_line / 2), #' \dontrun{
vjust = 1), #' library(ggplot2)
axis.title.x.top = element_text(margin = margin(b = half_line / 2), #'
vjust = 0), #' # Default editorial theme
axis.title.y = element_text( #' ggplot(mpg, aes(displ, hwy)) +
angle = 90, #' geom_point() +
margin = margin(r = half_line / #' theme_civilytics()
2), #'
vjust = 1 #' # With both gridlines and brand colors
), #' ggplot(mpg, aes(displ, hwy, colour = class)) +
axis.title.y.right = element_text( #' geom_point() +
angle = -90, #' scale_color_civilytics() +
margin = margin(l = half_line / 2), #' theme_civilytics(grid = "both")
vjust = 0 #'
), #' # Transparent background for embedding
legend.background = element_blank(), #' ggplot(mpg, aes(displ, hwy)) +
legend.spacing = unit(font_size, "pt"), #' geom_point() +
legend.spacing.x = NULL, #' theme_civilytics(paper_bg = FALSE)
legend.spacing.y = NULL, #'
legend.margin = margin(0, #' # Larger text for poster or display
0, 0, 0), #' ggplot(mpg, aes(displ, hwy)) +
legend.key = element_blank(), #' geom_point() +
legend.key.size = unit(1.1 * #' theme_civilytics(font_size = 18)
font_size, "pt"), #' }
legend.key.height = NULL, theme_civilytics <- function(
legend.key.width = NULL, font_size = 14,
legend.text = element_text(size = rel(rel_small)), font_family = CV_FONT_SANS,
legend.text.align = NULL, title_family = CV_FONT_DISPLAY,
legend.title = element_text(hjust = 0), line_size = 0.5,
legend.title.align = NULL, rel_small = 12 / 14,
legend.position = "right", rel_tiny = 11 / 14,
legend.direction = NULL, rel_large = 20 / 14,
legend.justification = c("left", ink = unname(civilytics_colors["ink"]),
"center"), paper = unname(civilytics_colors["paper"]),
legend.box = NULL, accent = unname(civilytics_colors["ember_600"]),
legend.box.margin = margin(0, strip_color = unname(civilytics_colors["paper_2"]),
0, 0, 0), grid = c("y", "x", "both", "none"),
legend.box.background = element_blank(), paper_bg = TRUE) {
legend.box.spacing = unit(font_size, "pt"),
panel.background = element_blank(), grid <- match.arg(grid)
panel.border = element_blank(), half_line <- font_size / 2
panel.grid = element_blank(), small_size <- rel_small * font_size
panel.grid.major = NULL, rule_color <- unname(civilytics_colors["rule"])
panel.grid.minor = NULL, ink_2 <- unname(civilytics_colors["ink_2"])
panel.grid.major.x = NULL, ink_3 <- unname(civilytics_colors["ink_3"])
panel.grid.major.y = NULL, bg_color <- if (isTRUE(paper_bg)) paper else NA
panel.grid.minor.x = NULL,
panel.grid.minor.y = NULL, # Grid line elements
panel.spacing = unit(half_line, grid_line <- ggplot2::element_line(color = rule_color, linewidth = 0.35)
"pt"), no_line <- ggplot2::element_blank()
panel.spacing.x = NULL,
panel.spacing.y = NULL, ggplot2::theme_grey(
panel.ontop = FALSE, base_size = font_size,
strip.background = element_rect(fill = "grey80"), base_family = font_family,
strip.text = element_text( ink = ink,
size = rel(rel_small), paper = paper,
margin = margin(half_line / 2, half_line / accent = accent
2, half_line / 2, ) %+replace%
half_line / 2) ggplot2::theme(
), line = ggplot2::element_line(
strip.text.x = NULL, color = ink,
strip.text.y = element_text(angle = -90), linewidth = line_size,
strip.placement = "inside", linetype = 1,
strip.placement.x = NULL, lineend = "butt"
strip.placement.y = NULL, ),
strip.switch.pad.grid = unit(half_line / 2, rect = ggplot2::element_rect(
"pt"), fill = NA,
strip.switch.pad.wrap = unit(half_line / 2, color = NA,
"pt"), linewidth = line_size,
plot.background = element_blank(), linetype = 1
plot.title = element_text( ),
face = "bold", text = ggplot2::element_text(
size = rel(rel_large), family = font_family,
hjust = 0, face = "plain",
vjust = 1, color = ink,
margin = margin(b = half_line) size = font_size,
), hjust = 0.5,
plot.subtitle = element_text( vjust = 0.5,
size = rel(rel_small), angle = 0,
hjust = 0, lineheight = 0.9,
vjust = 1, margin = ggplot2::margin(),
margin = margin(b = half_line) debug = FALSE
), ),
plot.caption = element_text( # -- Axes --
size = rel(rel_tiny), axis.line = ggplot2::element_blank(),
hjust = 0, # set hjust to 0 axis.line.x = ggplot2::element_line(
vjust = 1, color = ink,
lineheight = 1, linewidth = 0.6,
margin = margin(t = half_line) lineend = "square"
), ),
plot.tag = element_text( axis.line.y = ggplot2::element_blank(),
face = "bold", axis.text = ggplot2::element_text(
hjust = 0, color = ink_2,
vjust = 0.7 size = ggplot2::rel(rel_small)
), ),
plot.tag.position = c(0, 1), axis.text.x = ggplot2::element_text(
plot.margin = margin(half_line, margin = ggplot2::margin(t = small_size / 4),
half_line, half_line, half_line), vjust = 1
complete = TRUE ),
) 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()
}
+323 -3
View File
@@ -78,7 +78,7 @@ pretty_count <- function(x) {
#' Unsuppress data using sampling #' Unsuppress data using sampling
#' #'
#' @param x #' @param x a vector
#' @param replace_char the character you want to replace in the vector #' @param replace_char the character you want to replace in the vector
#' @param zeros the number of zeroes to oversample when replacing replace_char #' @param zeros the number of zeroes to oversample when replacing replace_char
#' @param max_value the numeric maximum value the replacement for the "*" can be #' @param max_value the numeric maximum value the replacement for the "*" can be
@@ -103,10 +103,11 @@ star_subs <- function(x, replace_char = "*",
#' #'
#' @param x character description of grade levels from NCES style data #' @param x character description of grade levels from NCES style data
#' #'
#' @return #' @return a numeric vector
#' @export #' @export
#' #'
#' @examples #' @examples
#' grade_level_to_num(c("KG", "Pre-K", "12", "10", "09"))
grade_level_to_num <- function(x) { grade_level_to_num <- function(x) {
# Cannot generate new levels if it is a factor so we coerce to character first # Cannot generate new levels if it is a factor so we coerce to character first
x <- as.character(x) x <- as.character(x)
@@ -122,10 +123,11 @@ grade_level_to_num <- function(x) {
#' #'
#' @param x a character vector with NCES race codes, often from Urban Institute #' @param x a character vector with NCES race codes, often from Urban Institute
#' #'
#' @return #' @return recoded race categories following NCES race codes
#' @export #' @export
#' #'
#' @examples #' @examples
#' race_short_names(c("Black", "Hispanic Or Latino", "Two Or More Races"))
race_short_names <- function(x) { race_short_names <- function(x) {
x <- as.character(x) x <- as.character(x)
x[x %in% c("Black", "Black Or African American", "Black or African American", x[x %in% c("Black", "Black Or African American", "Black or African American",
@@ -144,4 +146,322 @@ race_short_names <- function(x) {
return(x) return(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
#'
#' @examples
#' x <- c(2, NA, 4, 9)
#' na_sum(x) # 15
#' na_sum(x, quiet = TRUE) # 15 (no message)
na_sum <- function(x, quiet = FALSE) {
stopifnot(is.numeric(x))
if (!quiet) {
message("Taking a sum with missing values equal to 0, be careful!")
}
x <- na_zero(x)
return(sum(x))
}
# This function looks up the appropriate postal code for states from the
# state name.
# It also substitutes in PR and DC for Puerto Rico and District of Columbia
# which are not included in the lookup table of states and state abbreviations
# that comes with R.
#' Title
#'
#' @param x a vector of state names
#' @return state abbreviations matching state naems provided in X
#' @export
#'
#' @examples
#' postcode_lookup("Montana")
postcode_lookup <- function(x) {
modify_name <- c(state.name, "District of Columbia", "Puerto Rico")
modify_abb <- c(state.abb, "DC", "PR")
abb <- modify_abb[match(x, modify_name)]
return(abb)
}
#' Truncated matching function
#'
#' @param x, the character value to match
#' @param y, a vector of multiple character values to look for a match in
#' @param n, an integer, how many matches to return
#' @importFrom stringdist stringsim
#'
#' @return an integer giving the position of the table with the most characters
trunc_match <- function(x, y, n) {
out <- y[order(stringdist::stringsim(x, y, method = "lv"),
decreasing = TRUE)]
if (length(out) < n) {
n <- length(out)
}
out <- out[1:n]
return(out)
}
#' Compute the outersection of two fectors
#'
#' @param x first vector, of any type
#' @param y second vector, same type as x
#' @param ... additional vectors to be checked
#'
#' @return unique values across all of the vectors
#' @export
#'
#' @examples
#' # desired result is c(1, 2, 3, 6, 9, 10)
#' outersect(1:5, 4:8, 7:10)
outersect <- function(x, y, ...) {
big.vec <- c(x, y, ...)
duplicates <- big.vec[duplicated(big.vec)]
setdiff(big.vec, unique(duplicates))
}
# desired result is c(1, 2, 3, 6, 9, 10)
#outersect(1:5, 4:8, 7:10)
#[1] 1 2 3 6 9 10
#' Get the FIPS code for a given state abbreviation
#'
#' @param stabbr a two letter abbreviation for a US state
#'
#' @return FIPS codes that match the abbreviation
#' @export
#'
#' @examples
#' \dontrun{
#' get_fips("MT")
#' get_fips("PR")
#' }
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]
return(out)
}
#' Get the state abbreviation from a given FIPS Code
#'
#' @param fips a character value that captures the FIPS code with leading 0
#'
#' @return a character value, length 2, with the state abbreviation
#' @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) {
out <- rep(NA, length(fips))
for (i in length(fips)) {
out[i] <- fips_codes[fips_codes$state_code == fips, 1]
}
return(out)
} else {
out <- fips_codes[fips_codes$state_code == fips, 1]
return(out)
}
}
## Let's calculate the z-score for the gap as well
# Test statistic needs 4 values
# Proporation A, Numerator A
# Proportion B, Numerator B
#' Calculate a Z-Score for a comparison between two proportions
#'
#' @param a_prop the proportion for group a
#' @param a_count the count of the population in group a
#' @param b_prop the proportion for group b
#' @param b_count the count of the population in group b
#'
#' @return a numeric z score
#' @export
#'
#' @examples
#' z_gap_test(0.0002, 1e4, 0.0003, 1e4)
z_gap_test <- function(a_prop, a_count, b_prop, b_count) {
num <- (a_prop - b_prop) - 0
denom_a <- (a_prop * (1-a_prop)) / a_count
denom_b <- (b_prop * (1-b_prop)) / b_count
denom <- sqrt(denom_a + denom_b)
z = num / denom
#if (is.nan)
return(z)
}
# TODO: Consider vectorizing
#z_univariate_v <- Vectorize(z_univariate, SIMPLIFY = TRUE)
#' Add a random jitter to a count variable to mask its true value
#'
#' @param x the vector of numerics
#' @param fac the range of values to add or subtract to perturb the count
#'
#' @details The count in the name means that this function enforces a floor of
#' 0 on values, so values perturbed to have less than 0 will be capped at 0.
#'
#' @return a numeric vector
#' @export
#'
#' @examples
#' perturb_count(20:30, fac = 3)
perturb_count <- function(x, fac = 3) {
x <- sapply(x, function(x) x + sample(-fac:fac, 1))
x[x < 0] <- 0
return(x)
}
#' Add random noise to a variable before rounding
#'
#' @param x a numeric we want to round
#'
#' @return rounded values
#' @export
#' @details Credit to Jens von Bergmann for this algo https://github.com/mountainMath/dotdensity/blob/master/R/dot-density.R
#' @importFrom stats runif
#'
#' @examples
#' random_round(1.93)
random_round <- function(x) {
v = as.integer(x)
r = x-v
test = runif(length(r), 0.0, 1.0)
add = rep(as.integer(0),length(r))
add[r>test] <- as.integer(1)
value = v + add
ifelse(is.na(value) | value<0, 0, value)
return(value)
}
#' Safely take a ratio and do not fail if 0 is in the denominator
#'
#' @param num numerator, a numeric
#' @param denom denominator, a numeric
#'
#' @return The proportion, safely calculated with 0.1 substituting for 0
#' @export
#'
#' @examples
#' safe_ratio(100, 1)
#' safe_ratio(100, 0)
safe_ratio <- function(num, denom) {
denom <- ifelse(denom == 0, 0.1, denom)
y <- num / denom
return(y)
}
#' Take the maximum of a number after trimming values
#'
#' @param vec a numeric vector
#' @param n integer, the number of maximum values to trim before taking the maximum
#'
#' @return the highest value after removing the highest n values
#' @export
#'
#' @examples
#' trim_max(c(10, 10, 10, 9, 8, 7), n = 2)
#' trim_max(c(10, 10, 10, 9, 8, 7), n = 3)
#' trim_max(c(10, 10, 10, 9, 8, 7), n = 4)
trim_max <- function(vec, n) {
# Sort vector ascending
sorted_vec <- sort(vec)
end_point <- length(vec) - n
if (end_point <= 0) {
return(1)
}
# Exclude n largest (most extreme) values
filtered_vec <- sorted_vec[(1:(length(vec)-n))]
# Find the maximum value among excluded values if any exist
max_value <- max(filtered_vec, na.rm = TRUE)
return(max_value)
}
#' Round values to the nearest 0.5
#'
#' @param x a numeric vector to round
#'
#' @return a numeric vector with all elements rounded to 0, 0.5, or 1
#' @export
#'
#' @examples
#' round_to_nearest_half(0.9)
#' round_to_nearest_half(0.7)
#' round_to_nearest_half(0.4)
round_to_nearest_half <- function(x) {
if (x %% 1 == 0) { # If x is already an integer, no change needed
return(as.integer(x))
} else {
decimal_part <- x - floor(x)
if (decimal_part >= 0.25 & decimal_part < 0.75) {
rounded_x <- floor(x) + 0.5
} else {
rounded_x <- round(x, 0)
}
return(rounded_x)
}
}
#' Round values to the nearest 0.5
#'
#' @inheritParams round_to_nearest_half
#'
#' @return a numeric vector with all elements rounded to 0, 0.5, or 1
#' @export
#'
#' @examples
#' rnh(c(0.2, 0.3, 0.4, 0.8, 0.09, 0.9))
rnh <- function(x) {
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")
```
+258 -25
View File
@@ -1,25 +1,258 @@
# civilytics # civilytics
<!-- badges: start --> Brand themes, color palettes, and utility functions for [Civilytics
<!-- badges: end --> Consulting](https://www.civilytics.com). The package provides a complete
ggplot2 theme system drawn from the Civilytics design system — warm
The goal of civilytics is to ... paper backgrounds, civic-navy ink, and editorial typography — along with
10 curated color palettes, logo composition helpers, and data-wrangling
## Installation utilities for public-sector analysis.
You can install the released version of civilytics from [CRAN](https://CRAN.R-project.org) with: ## Installation
``` r Install from the Civilytics Gitea server:
install.packages("civilytics")
``` ``` r
# install.packages("remotes")
## Example remotes::install_git("https://gitea.civilytics.org/Civilytics/civilyticsR.git")
```
This is a basic example which shows you how to solve a common problem:
## Quick start
``` r
library(civilytics) ``` r
## basic example code library(civilytics)
``` library(ggplot2)
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point(size = 2.5) +
scale_color_civilytics() +
labs(
title = "Fuel economy by engine displacement",
subtitle = "Highway MPG vs. engine size for 234 vehicles",
caption = "Source: EPA fuel economy data (ggplot2::mpg)",
x = "Engine displacement (litres)",
y = "Highway MPG"
) +
theme_civilytics()
```
<img src="man/figures/README-quickstart-1.png" alt="" width="100%" />
## Color palettes
The package ships 53 named brand colors in `civilytics_colors` and 10
curated palettes in `civilytics_palettes`. Use `civilytics_palette()` to
retrieve colors by name, or pass palettes directly to the ggplot2
scales.
### Palette gallery
<img src="man/figures/README-palette-gallery-1.png" alt="" width="100%" />
### Using palettes
``` r
# Discrete fill with the qualitative palette
ggplot(mpg, aes(class, fill = class)) +
geom_bar(show.legend = FALSE) +
scale_fill_civilytics() +
labs(title = "Vehicle counts by class", x = NULL, y = NULL) +
theme_civilytics(grid = "y")
```
<img src="man/figures/README-palette-usage-1.png" alt="" width="100%" />
``` r
# Continuous fill with a sequential palette
ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
geom_tile() +
scale_fill_civilytics("seq_ember", discrete = FALSE) +
labs(title = "Old Faithful eruption density") +
theme_civilytics(grid = "none")
```
<img src="man/figures/README-palette-usage-2.png" alt="" width="100%" />
## Themes
Three theme variants cover the most common output contexts. All share
the same typographic structure and accept `grid` and `paper_bg`
parameters.
### Editorial (default)
The default theme uses a warm paper background with horizontal gridlines
— an editorial, Pew-style layout.
``` r
base_plot <- ggplot(mpg, aes(displ, hwy)) +
geom_point(aes(colour = factor(cyl)), size = 2) +
scale_color_civilytics() +
labs(
title = "Engine size vs. highway fuel economy",
subtitle = "Colored by number of cylinders",
caption = "Source: ggplot2::mpg",
colour = "Cylinders",
x = "Displacement (L)", y = "Highway MPG"
)
base_plot + theme_civilytics()
```
<img src="man/figures/README-theme-editorial-1.png" alt="" width="100%" />
### Grid options
The `grid` parameter controls which major gridlines are drawn.
<img src="man/figures/README-theme-grids-1.png" alt="" width="100%" />
### Dark
Dark navy background with light text, suitable for presentations or
dashboards on dark surfaces.
``` r
base_plot + theme_civilytics_dark()
```
<img src="man/figures/README-theme-dark-1.png" alt="" width="100%" />
### Slide
Transparent background and larger base font (18 pt), sized for Reveal.js
slides or PowerPoint exports.
``` r
base_plot + theme_civilytics_slide()
```
<img src="man/figures/README-theme-slide-1.png" alt="" width="100%" />
### Facets
Facet strips use the `paper_2` tint, with the title font.
``` r
ggplot(mpg, aes(displ, hwy)) +
geom_point(colour = civilytics_colors["navy_600"], size = 1.5) +
facet_wrap(~class, ncol = 4) +
labs(
title = "Highway MPG by vehicle class",
x = "Displacement (L)", y = "Highway MPG"
) +
theme_civilytics(grid = "y")
```
<img src="man/figures/README-theme-facets-1.png" alt="" width="100%" />
## Logo utilities
Add the Civilytics logo to any ggplot using the pipe-friendly
`civilytics_logo()` or the lower-level `add_logo()` /
`make_logo_grob()`. 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
+80
View File
@@ -0,0 +1,80 @@
---
title: "Who pays when rent outpaces wages?"
subtitle: "A 12-county analysis of cost-burdened renter households, 2019–2024."
author:
- name: "Civilytics Research"
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: ../typst/civilytics-typst.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 Research, 2026
## Thank you {.thank-you}
Questions?
- jared@civilytics.com
- civilytics.consulting
@@ -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
+41
View File
@@ -0,0 +1,41 @@
% Civilytics — title-page partial.
% Replaces Quarto's default \maketitle. Uses values from YAML
% (\thetitle, \theauthor, \thedate) plus an \ifabstract block.
\begin{titlepage}
\pagecolor{paper}
\color{ink}
\vspace*{0.5in}
{\sffamily\bfseries\scriptsize\color{ember}\MakeUppercase{— Civilytics Research}\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}
\begin{tikzpicture}[overlay, remember picture]
\end{tikzpicture}
{\color{ember}\rule{40pt}{2pt}}
\end{center}
\end{titlepage}
\clearpage
+112
View File
@@ -0,0 +1,112 @@
% =============================================================
% 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) ---
\setmainfont{Source Serif 4}[
UprightFont = *,
ItalicFont = * Italic,
BoldFont = * SemiBold,
BoldItalicFont = * SemiBold Italic,
Ligatures = TeX,
]
\setsansfont{Inter}[Ligatures = TeX]
\setmonofont{JetBrains Mono}[Scale = 0.92]
\newfontfamily\displayfont{Libre Franklin}[Ligatures = TeX]
% --- 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.consulting}
\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;
}
+244
View File
@@ -0,0 +1,244 @@
// =============================================================
// Civilytics — Typst template for Quarto PDF
// Usage in YAML:
// format:
// typst:
// template: quarto/typst/civilytics-typst.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.consulting],
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 Research]
]
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
}
// Quarto entry point
#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
)
$body$
+36 -21
View File
@@ -1,21 +1,36 @@
% Generated by roxygen2: do not edit by hand % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/logo.R % Please edit documentation in R/logo.R
\name{add_logo} \name{add_logo}
\alias{add_logo} \alias{add_logo}
\title{Add a logo to a ggplot2 object} \title{Add a logo to a ggplot2 object}
\usage{ \usage{
add_logo(plot, logo, margin_param = NULL) add_logo(
} plot,
\arguments{ logo,
\item{plot}{a ggplot2 grob} margin_param = NULL,
font_scale = 1.1,
\item{logo}{a logo grob created by make_logo_grob()} position = c("bottom-right", "bottom-left", "top-right", "top-left")
)
\item{margin_param}{a numeric specifying what margin to add or subtract to align the logo} }
} \arguments{
\value{ \item{plot}{a ggplot2 grob}
} \item{logo}{a logo grob created by make_logo_grob()}
\description{
Add a logo to a ggplot2 object \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
}
\description{
Add a logo to a ggplot2 object
}
+48 -36
View File
@@ -1,36 +1,48 @@
% Generated by roxygen2: do not edit by hand % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/logo.R % Please edit documentation in R/logo.R
\name{add_logo_ga} \name{add_logo_ga}
\alias{add_logo_ga} \alias{add_logo_ga}
\title{Add a logo to a ggplot2 object} \title{Add a logo to multiple ggplot2 objects}
\usage{ \usage{
add_logo_ga(plot_list, logo, nrow = 1, widths = NULL, margin_param = NULL) add_logo_ga(
} plot_list,
\arguments{ logo,
\item{plot_list}{a list containing ggplot2 objects} nrow = 1,
widths = NULL,
\item{logo}{a grob containing the logo created with `make_logo_grob`} margin_param = NULL,
font_scale = 1.1
\item{nrow}{an integer, default = 1, for the number of rows to align the plots in} )
}
\item{widths}{an optional vector the same length as plot_list with the widths for each plot} \arguments{
\item{plot_list}{a list containing ggplot2 objects}
\item{margin_param}{a number giving the adjustment up or down to help manually align logo and captions}
} \item{logo}{a grob containing the logo created with `make_logo_grob`}
\value{
\item{nrow}{an integer, default = 1, for the number of rows to align the plots in}
}
\description{ \item{widths}{an optional vector the same length as plot_list with the widths for each plot}
Add a logo to a ggplot2 object
} \item{margin_param}{a number giving the adjustment up or down to help manually align logo and captions}
\note{
The resulting object needs to be drawn to the screen using grid.draw() \item{font_scale}{Numeric. Multiplicative scaling factor applied to text
} sizes before composing. Default `1.1`. See [add_logo()] for details.}
\examples{ }
library(ggplot2); library(grid) \value{
tmp_plot <- ggplot(mtcars) + aes(x = hp, y = disp) + geom_point() + theme_civilytics() a grid object
tmp_logo <- make_logo_grob() }
plot_and_logo <- add_logo(tmp_plot, tmp_logo) \description{
grid.draw(plot_and_logo) Add a logo to multiple ggplot2 objects
dev.off() }
} \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()
plot_and_logo <- add_logo(tmp_plot, tmp_logo)
grid.draw(plot_and_logo)
dev.off()
}
}
+25
View File
@@ -0,0 +1,25 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/prop_conf.R
\name{agresti_coull_interval}
\alias{agresti_coull_interval}
\title{Calculate the Agresti-Coull interval}
\usage{
agresti_coull_interval(num, den, conf.level = 0.95)
}
\arguments{
\item{num}{number of successes}
\item{den}{number of trials}
\item{conf.level}{default 0.95, confidence level for the interval}
}
\value{
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")
}
}
+54
View File
@@ -0,0 +1,54 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/civilytics-package.R
\docType{package}
\name{civilytics-package}
\alias{civilytics}
\alias{civilytics-package}
\title{civilytics: Brand Themes, Color Palettes, and Utility Functions for Civilytics}
\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+)
}
}
\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}
+21
View File
@@ -0,0 +1,21 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/prop_conf.R
\name{clopper_pearson}
\alias{clopper_pearson}
\title{Get a simple Clopper Pearson interval}
\usage{
clopper_pearson(num, den, conf.level = 0.95)
}
\arguments{
\item{num}{number of successes}
\item{den}{number of trials}
\item{conf.level}{default 0.95, set the confidence interval to return}
}
\value{
three values forming the upper and lower bounds of the confidence region and the true value
}
\description{
Get a simple Clopper Pearson interval
}
+17 -17
View File
@@ -1,17 +1,17 @@
% Generated by roxygen2: do not edit by hand % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/db.R % Please edit documentation in R/db.R
\name{countDots} \name{countDots}
\alias{countDots} \alias{countDots}
\title{Count the number of single period entries in a vector} \title{Count the number of single period entries in a vector}
\usage{ \usage{
countDots(x) countDots(x)
} }
\arguments{ \arguments{
\item{x}{a character vector} \item{x}{a character vector}
} }
\value{ \value{
An integer counting the number of "." occurences in a vector
} }
\description{ \description{
Count the number of single period entries in a vector Count the number of single period entries in a vector
} }
+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

+23
View File
@@ -0,0 +1,23 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{get_fips}
\alias{get_fips}
\title{Get the FIPS code for a given state abbreviation}
\usage{
get_fips(stabbr)
}
\arguments{
\item{stabbr}{a two letter abbreviation for a US state}
}
\value{
FIPS codes that match the abbreviation
}
\description{
Get the FIPS code for a given state abbreviation
}
\examples{
\dontrun{
get_fips("MT")
get_fips("PR")
}
}
+17 -20
View File
@@ -1,20 +1,17 @@
% Generated by roxygen2: do not edit by hand % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/logo.R % Please edit documentation in R/logo.R
\name{get_png} \name{get_png}
\alias{get_png} \alias{get_png}
\title{Plot a PNG file as a rasterGrob for inclusion in ggplot2} \title{Plot a PNG file as a rasterGrob for inclusion in ggplot2}
\usage{ \usage{
get_png(filename) get_png(filename)
} }
\arguments{ \arguments{
\item{filename}{a character with file path to a png file} \item{filename}{a character with file path to a png file}
} }
\value{ \value{
a plotted rasteGrob of a png image
} }
\description{ \description{
Plot a PNG file as a rasterGrob for inclusion in ggplot2 Plot a PNG file as a rasterGrob for inclusion in ggplot2
} }
\examples{
}
+22
View File
@@ -0,0 +1,22 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{get_stabbr}
\alias{get_stabbr}
\title{Get the state abbreviation from a given FIPS Code}
\usage{
get_stabbr(fips)
}
\arguments{
\item{fips}{a character value that captures the FIPS code with leading 0}
}
\value{
a character value, length 2, with the state abbreviation
}
\description{
Get the state abbreviation from a given FIPS Code
}
\examples{
\dontrun{
get_stabbr("06")
}
}
+20 -17
View File
@@ -1,17 +1,20 @@
% Generated by roxygen2: do not edit by hand % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R % Please edit documentation in R/utils.R
\name{grade_level_to_num} \name{grade_level_to_num}
\alias{grade_level_to_num} \alias{grade_level_to_num}
\title{Recode grade level from character to numeric} \title{Recode grade level from character to numeric}
\usage{ \usage{
grade_level_to_num(x) grade_level_to_num(x)
} }
\arguments{ \arguments{
\item{x}{character description of grade levels from NCES style data} \item{x}{character description of grade levels from NCES style data}
} }
\value{ \value{
a numeric vector
} }
\description{ \description{
Recode grade level from character to numeric Recode grade level from character to numeric
} }
\examples{
grade_level_to_num(c("KG", "Pre-K", "12", "10", "09"))
}
+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 Test whether a ggplot2 object has a caption
} }
\examples{ \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(p1) # FALSE
} }
+25 -6
View File
@@ -4,16 +4,35 @@
\alias{make_logo_grob} \alias{make_logo_grob}
\title{Get a Civilytics Logo grob} \title{Get a Civilytics Logo grob}
\usage{ \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{ \value{
a gg object which contains the logo file stored as a Grob suitable for manipulating in A ggplot object (class `"gg"`) containing the logo grob.
grid
} }
\description{ \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{ \examples{
logo <- make_logo_grob() logo <- make_logo_grob() # wordmark, light
class(logo) # gg logo <- make_logo_grob("mark", "dark") # mark, dark
logo <- make_logo_grob(position = "bottom-left") # left-aligned
class(logo) # "gg" "ggplot"
} }
+26
View File
@@ -0,0 +1,26 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/join_utilities.R
\name{match_test}
\alias{match_test}
\title{Test the join between two sets of identifiers}
\usage{
match_test(x, y, distinct = TRUE)
}
\arguments{
\item{x}{a vector of identifiers to check against y}
\item{y}{a vector of identifiers to check against x}
\item{distinct}{logical, should duplicate values of x and y be removed before testing}
}
\value{
nothing, print a summary of match statistics to the console
}
\description{
Test the join between two sets of identifiers
}
\examples{
x <- LETTERS
y <- c(letters, LETTERS)
match_test(x, y)
}
+22 -22
View File
@@ -1,22 +1,22 @@
% Generated by roxygen2: do not edit by hand % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/logo.R % Please edit documentation in R/logo.R
\name{measure_caption} \name{measure_caption}
\alias{measure_caption} \alias{measure_caption}
\title{Measure a ggplot2 object caption} \title{Measure a ggplot2 object caption}
\usage{ \usage{
measure_caption(gg) measure_caption(gg)
} }
\arguments{ \arguments{
\item{gg}{a ggplot object} \item{gg}{a ggplot object}
} }
\value{ \value{
a numeric value stating the number of lines to be added or subtracted to align a logo with a numeric value stating the number of lines to be added or subtracted to align a logo with
the caption the caption
} }
\description{ \description{
Measure a ggplot2 object caption Measure a ggplot2 object caption
} }
\examples{ \examples{
p1 <- qplot(mpg, wt, data = mtcars) p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
measure_caption(p1) # Should equal 1 since no caption is required measure_caption(p1) # Should equal 1 since no caption is present
} }
+26
View File
@@ -0,0 +1,26 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{na_sum}
\alias{na_sum}
\title{Sum a numeric that contains missing values and ignore missing values}
\usage{
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
}
\description{
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)
}
+25
View File
@@ -0,0 +1,25 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{outersect}
\alias{outersect}
\title{Compute the outersection of two fectors}
\usage{
outersect(x, y, ...)
}
\arguments{
\item{x}{first vector, of any type}
\item{y}{second vector, same type as x}
\item{...}{additional vectors to be checked}
}
\value{
unique values across all of the vectors
}
\description{
Compute the outersection of two fectors
}
\examples{
# desired result is c(1, 2, 3, 6, 9, 10)
outersect(1:5, 4:8, 7:10)
}
+26
View File
@@ -0,0 +1,26 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{perturb_count}
\alias{perturb_count}
\title{Add a random jitter to a count variable to mask its true value}
\usage{
perturb_count(x, fac = 3)
}
\arguments{
\item{x}{the vector of numerics}
\item{fac}{the range of values to add or subtract to perturb the count}
}
\value{
a numeric vector
}
\description{
Add a random jitter to a count variable to mask its true value
}
\details{
The count in the name means that this function enforces a floor of
0 on values, so values perturbed to have less than 0 will be capped at 0.
}
\examples{
perturb_count(20:30, fac = 3)
}
+3 -1
View File
@@ -21,6 +21,8 @@ a rasterImage
Plot a jpeg image as a raster Plot a jpeg image as a raster
} }
\examples{ \examples{
img <- system.file("img","Civilytics Consulting Logo.jpg",package="civilytics") \dontrun{
img <- system.file("img","Knowles_Headshot_2019_good.jpg",package="civilytics")
plot_jpeg(img) plot_jpeg(img)
} }
}
+20
View File
@@ -0,0 +1,20 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{postcode_lookup}
\alias{postcode_lookup}
\title{Title}
\usage{
postcode_lookup(x)
}
\arguments{
\item{x}{a vector of state names}
}
\value{
state abbreviations matching state naems provided in X
}
\description{
Title
}
\examples{
postcode_lookup("Montana")
}
+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.
}
+20 -17
View File
@@ -1,17 +1,20 @@
% Generated by roxygen2: do not edit by hand % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R % Please edit documentation in R/utils.R
\name{race_short_names} \name{race_short_names}
\alias{race_short_names} \alias{race_short_names}
\title{Recode NCES race categories to shorter names} \title{Recode NCES race categories to shorter names}
\usage{ \usage{
race_short_names(x) race_short_names(x)
} }
\arguments{ \arguments{
\item{x}{a character vector with NCES race codes, often from Urban Institute} \item{x}{a character vector with NCES race codes, often from Urban Institute}
} }
\value{ \value{
recoded race categories following NCES race codes
} }
\description{ \description{
Recode NCES race categories to shorter names Recode NCES race categories to shorter names
} }
\examples{
race_short_names(c("Black", "Hispanic Or Latino", "Two Or More Races"))
}
+23
View File
@@ -0,0 +1,23 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{random_round}
\alias{random_round}
\title{Add random noise to a variable before rounding}
\usage{
random_round(x)
}
\arguments{
\item{x}{a numeric we want to round}
}
\value{
rounded values
}
\description{
Add random noise to a variable before rounding
}
\details{
Credit to Jens von Bergmann for this algo https://github.com/mountainMath/dotdensity/blob/master/R/dot-density.R
}
\examples{
random_round(1.93)
}
+20
View File
@@ -0,0 +1,20 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{rnh}
\alias{rnh}
\title{Round values to the nearest 0.5}
\usage{
rnh(x)
}
\arguments{
\item{x}{a numeric vector to round}
}
\value{
a numeric vector with all elements rounded to 0, 0.5, or 1
}
\description{
Round values to the nearest 0.5
}
\examples{
rnh(c(0.2, 0.3, 0.4, 0.8, 0.09, 0.9))
}
+22
View File
@@ -0,0 +1,22 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{round_to_nearest_half}
\alias{round_to_nearest_half}
\title{Round values to the nearest 0.5}
\usage{
round_to_nearest_half(x)
}
\arguments{
\item{x}{a numeric vector to round}
}
\value{
a numeric vector with all elements rounded to 0, 0.5, or 1
}
\description{
Round values to the nearest 0.5
}
\examples{
round_to_nearest_half(0.9)
round_to_nearest_half(0.7)
round_to_nearest_half(0.4)
}
+23
View File
@@ -0,0 +1,23 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{safe_ratio}
\alias{safe_ratio}
\title{Safely take a ratio and do not fail if 0 is in the denominator}
\usage{
safe_ratio(num, denom)
}
\arguments{
\item{num}{numerator, a numeric}
\item{denom}{denominator, a numeric}
}
\value{
The proportion, safely calculated with 0.1 substituting for 0
}
\description{
Safely take a ratio and do not fail if 0 is in the denominator
}
\examples{
safe_ratio(100, 1)
safe_ratio(100, 0)
}

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