Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76aebe2b49
|
||
|
|
446843bc86
|
||
|
|
04a5ee958b
|
||
|
|
cd1af67a3a
|
||
|
|
496b9ed9ac
|
||
|
|
7cad98b434
|
||
|
|
2c7c9a6dbc
|
||
|
|
6051e4bb5c
|
||
|
|
9dea017381 | ||
|
|
967e7e1111 | ||
|
|
f32093c662 | ||
|
|
e257180f15 | ||
|
|
0ef0f310e2 | ||
|
|
4fb5c488f8
|
||
|
|
96dc1c4c6b | ||
|
|
6ea9265236 | ||
|
|
b9d427eb76
|
||
|
|
a904d2313a
|
||
|
|
761a72da18 | ||
|
|
4f51a49903
|
||
|
|
5fc8155965
|
||
|
|
41162597cf | ||
|
|
2a74ec2bd3 | ||
|
|
366ac39fac | ||
|
|
fc8bf1866b | ||
|
|
75b9a73165 | ||
|
|
2f1cdcde52 | ||
|
|
136c53643c
|
||
|
|
41fe2b6170
|
||
|
|
ac8e3eb60e
|
||
|
|
98e0949d27
|
||
|
|
328d9d2fd6
|
||
|
|
95b8ba1da4
|
||
|
|
f134621df5
|
||
|
|
31d3e2886d
|
||
|
|
34622ccf88
|
||
|
|
f9b51cc558
|
||
|
|
875251068a
|
||
|
|
515bae8685
|
||
|
|
099fff1d1d
|
||
|
|
7371069379
|
||
|
|
3a6f20b723
|
||
|
|
c93c469267
|
||
|
|
2d70d9aecf
|
||
|
|
bf6f7cc480
|
||
|
|
b84fc2f7d0
|
||
|
|
d17a4fbc7c
|
||
|
|
53299c7e88
|
||
|
|
38cdaa8ea7
|
||
|
|
01c9d1c796 | ||
|
|
b59cbeb9b6 | ||
|
|
f3f13fcfe5 | ||
|
|
962f8ea6b7 | ||
|
|
f4a4ee85a7 | ||
|
|
82b8db7f84 | ||
|
|
4f49ae6d5a | ||
|
|
e27ff481c1 | ||
|
|
91dceb9c1c | ||
|
|
c6079eb515 | ||
|
|
0ad87eb333 | ||
|
|
ac63932a31 | ||
|
|
9acebb5133 | ||
|
|
a0b4abdb9e | ||
|
|
f0133e32b7 | ||
|
|
91dad76526 |
@@ -1,6 +1,12 @@
|
|||||||
^.*\.Rproj$
|
^.*\.Rproj$
|
||||||
^\.Rproj\.user$
|
^\.Rproj\.user$
|
||||||
^\.github$
|
^\.github$
|
||||||
|
^\.gitea$
|
||||||
^README\.Rmd$
|
^README\.Rmd$
|
||||||
|
^NEWS\.md$
|
||||||
^Makefile$
|
^Makefile$
|
||||||
^Jenkinsfile$
|
^Dockerfile$
|
||||||
|
^\.claude$
|
||||||
|
^\.playwright-mcp$
|
||||||
|
^civilytics-site\.png$
|
||||||
|
^Rplots\.pdf$
|
||||||
@@ -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}
|
||||||
@@ -2,3 +2,10 @@
|
|||||||
.Rhistory
|
.Rhistory
|
||||||
.RData
|
.RData
|
||||||
.Ruserdata
|
.Ruserdata
|
||||||
|
.compass-cache/
|
||||||
|
# roborev snapshots
|
||||||
|
/.roborev/
|
||||||
|
|
||||||
|
# Test run debris
|
||||||
|
tests/testthat/Rplots.pdf
|
||||||
|
tests/testthat/_problems/
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# roborev configuration, initialised by compass.
|
||||||
|
# Reviews are queued to a background daemon -- they never block a commit.
|
||||||
|
|
||||||
|
post_commit_review = 'commit'
|
||||||
|
# Scoped conventional commits (`chore(packaging):`) do not contain `chore:`,
|
||||||
|
# and this repo's cadence rule mandates the scope -- so both forms are listed.
|
||||||
|
excluded_commit_patterns = ['WIP', 'chore:', 'chore(', 'docs:', 'docs(', 'Merge ']
|
||||||
|
|
||||||
|
review_guidelines = '''
|
||||||
|
# --- compass:begin (generated -- edit the sources, not this) ---
|
||||||
|
- Prefer returning new values to mutating arguments in place. A function that edits
|
||||||
|
its caller's object is a bug waiting for a second caller.
|
||||||
|
- Validate at system boundaries -- user input, API responses, file contents, config.
|
||||||
|
Fail fast with a message naming the field and the file.
|
||||||
|
- Never swallow an error. Handle it or let it propagate; a bare catch that continues
|
||||||
|
is worse than a crash.
|
||||||
|
- No hardcoded secrets, tokens, or credentials, and no secrets in log output or error
|
||||||
|
messages.
|
||||||
|
- Parameterise every query. String-built SQL is a defect even when the input looks safe.
|
||||||
|
- Keep functions under roughly 50 lines and files under roughly 400. Flag nesting
|
||||||
|
deeper than four levels.
|
||||||
|
- No magic numbers or hardcoded paths -- name them as constants or read them from config.
|
||||||
|
- New behaviour needs a test. A bug fix needs a test that fails without the fix.
|
||||||
|
- Prose a person reads -- an issue title or body, a journal entry, a decision record,
|
||||||
|
the narrative on the status board -- names the action or the thing, not the shape of
|
||||||
|
the machinery. Flag "gate", "seam", "surface area", "load-bearing", "first-class",
|
||||||
|
"primitive", "blast radius". A project's own defined vocabulary is not the target.
|
||||||
|
- Use the native pipe `|>`, not magrittr `%>%`.
|
||||||
|
- snake_case for objects and functions; UPPER_SNAKE for constants. Never use `.` as a
|
||||||
|
word separator in a function name -- it collides with S3 dispatch.
|
||||||
|
- Validate arguments at the top of exported functions with `stopifnot()` or an explicit
|
||||||
|
check, and say which argument was wrong.
|
||||||
|
- Never `setDT()`, `set()`, or otherwise modify by reference a data.table the caller
|
||||||
|
still owns. `as.data.table()` copies; use it.
|
||||||
|
- Prefer `vapply()` to `sapply()` -- `sapply()` silently returns a list when the type
|
||||||
|
varies, which turns a type error into a downstream mystery.
|
||||||
|
- Use `seq_len(n)` / `seq_along(x)`, never `1:n`, which iterates backwards when n is 0.
|
||||||
|
- Compare strings with `==` only after checking for NA; use `identical()` for scalars
|
||||||
|
where NA would be wrong.
|
||||||
|
- Do not call `library()` inside package or module files; attach packages in scripts and
|
||||||
|
test helpers only.
|
||||||
|
- Namespace-qualify calls into other packages (`stats::sd`) in code that is sourced.
|
||||||
|
- Every exported function needs roxygen with `@param` for each argument (type, meaning,
|
||||||
|
and why the default is what it is) and `@return`. Add `@examples` for exported API.
|
||||||
|
- Declare dependencies in DESCRIPTION. Prefer base R or an existing dependency over
|
||||||
|
adding a new one; a package with zero hard deps is worth keeping that way.
|
||||||
|
- Signal errors with `stop()` carrying a condition class, so callers can catch the kind
|
||||||
|
rather than matching on message text.
|
||||||
|
- Keep internals internal. Export only what a user needs; an accidentally exported
|
||||||
|
helper becomes an API you have to keep.
|
||||||
|
- Tests use testthat edition 3. Each test is self-sufficient -- no reliance on state
|
||||||
|
left by an earlier test or on a fixture built elsewhere in the file.
|
||||||
|
- Prefer duplication in tests over a helper that hides what is being asserted.
|
||||||
|
- Brand colors come from `civilytics_colors`; never write a hex literal in theme, scale, logo, or table code. `R/flextable.R` still carries off-brand Bootstrap defaults (#2c3e50, #f0f0eb, #888888, #cccccc, #555555) -- do not add more.
|
||||||
|
- No tidyverse dependency. Imports is base R plus ggplot2, grid/gridExtra, png/jpeg, stringr, stringdist, showtext/sysfonts, jsonlite. Reject dplyr, purrr, magrittr, tibble, and data.table; use base R idioms.
|
||||||
|
- NAMESPACE is roxygen-generated. Declare imports with `@importFrom` in the roxygen block and re-run roxygen; never hand-edit NAMESPACE.
|
||||||
|
- Themes default to a transparent background (`paper_bg = FALSE`) so plots composite onto any Quarto, Reveal, or Typst background. Making an opaque background the default is a regression, not a preference.
|
||||||
|
- Theme functions thread `ink`/`paper`/`accent` into ggplot2 4.0's base-theme parameters rather than setting element colors ad hoc. ggplot2 >= 4.0 is a hard dependency, so use S7 `@` property access on plot and theme objects, not `$`.
|
||||||
|
- User-facing progress goes through `message()` so callers can suppress it. No `cat()` or `print()` in package code.
|
||||||
|
- Nothing personal or client-identifying is vendored into `inst/` -- brand assets only. Version 0.3.2 removed headshots for exactly this reason.
|
||||||
|
- The camelCase exports (`countCleanr`, `dbSafeNames`, `simpleCap`, `waldInterval`, `countDots`, `countNA`, `findDots`, `nvals`) are frozen public API; do not rename them. New functions are snake_case.
|
||||||
|
- `R/theme.R` and `R/logo.R` are long by design -- one file per surface, with dense roxygen. File length is not a finding in this package; flag a single function over roughly 80 lines instead.
|
||||||
|
# --- compass:end ---
|
||||||
|
'''
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
`civilytics` is the house R package for Civilytics Consulting: a ggplot2 brand theme
|
||||||
|
system, curated palettes, logo composition, Quarto/Typst/Reveal templates, and
|
||||||
|
data-wrangling helpers for public-sector analysis. It is a library other projects
|
||||||
|
depend on, so a breaking change here breaks reports that are already published.
|
||||||
|
|
||||||
|
## Project tracking
|
||||||
|
|
||||||
|
This repo is managed with compass. `pm/compass.toml`
|
||||||
|
defines the workstreams; `pm/JOURNAL.md` records sessions; `pm/decisions/` holds
|
||||||
|
numbered, immutable decision records. Ask compass where things stand rather than
|
||||||
|
reading the config back.
|
||||||
|
|
||||||
|
Workstreams (the `ws/` label on every issue):
|
||||||
|
|
||||||
|
| `ws/` | covers |
|
||||||
|
|---|---|
|
||||||
|
| `theme` | `R/theme.R`, `R/colors.R`, `R/fonts.R` — themes, palettes, font loading |
|
||||||
|
| `logo` | `R/logo.R`, `R/flextable.R`, `inst/img/` — logo and branded output |
|
||||||
|
| `quarto` | `R/quarto.R`, `inst/quarto/` — HTML, PDF, Typst, Reveal templates |
|
||||||
|
| `helpers` | `R/utils.R`, `R/prop_conf.R`, `R/join_utilities.R`, `R/db.R`, `R/notifications.R` |
|
||||||
|
| `packaging` | `DESCRIPTION`, `NAMESPACE`, `Makefile`, `Dockerfile`, `.gitea/workflows/` |
|
||||||
|
|
||||||
|
## Commit cadence
|
||||||
|
|
||||||
|
One coherent unit per commit, subject line:
|
||||||
|
|
||||||
|
```
|
||||||
|
type(ws): subject (#N)
|
||||||
|
```
|
||||||
|
|
||||||
|
`type` is one of `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`.
|
||||||
|
`ws` is a workstream id from the table above. `#N` is the issue, when there is one.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
`pm/compass.toml` is authoritative for these rules. Its `[roborev]
|
||||||
|
project_guidelines` are composed into `.roborev.toml`, so change them there and
|
||||||
|
re-run compass rather than editing `.roborev.toml` by hand. The prose below is the
|
||||||
|
same rules stated for a human reader; if the two ever disagree, `pm/compass.toml`
|
||||||
|
wins.
|
||||||
|
|
||||||
|
**Dependencies.** Base R plus ggplot2 (>= 4.0), grid/gridExtra, png/jpeg, stringr,
|
||||||
|
stringdist, showtext/sysfonts, jsonlite. No tidyverse: no dplyr, purrr, magrittr,
|
||||||
|
tibble, or data.table. Prefer an existing dependency or base R over adding one —
|
||||||
|
this package is installed into other people's environments.
|
||||||
|
|
||||||
|
**Brand colors** live in `civilytics_colors` and nowhere else. Never write a hex
|
||||||
|
literal in theme, scale, logo, or table code. (`R/flextable.R` still carries
|
||||||
|
off-brand Bootstrap defaults; that is known debt, not a pattern to copy.)
|
||||||
|
|
||||||
|
**Themes** default to a transparent background (`paper_bg = FALSE`) so plots
|
||||||
|
composite onto any Quarto, Reveal, or Typst background. Thread `ink`, `paper`, and
|
||||||
|
`accent` into ggplot2 4.0's base-theme parameters rather than setting element colors
|
||||||
|
one at a time. ggplot2 >= 4.0 is a hard dependency, so use S7 `@` property access on
|
||||||
|
plot and theme objects, not `$`.
|
||||||
|
|
||||||
|
**Naming.** New functions are snake_case. The camelCase exports (`countCleanr`,
|
||||||
|
`dbSafeNames`, `simpleCap`, `waldInterval`, `countDots`, `countNA`, `findDots`,
|
||||||
|
`nvals`) are frozen public API — do not rename them.
|
||||||
|
|
||||||
|
**Documentation.** Roxygen generates both `man/` and `NAMESPACE`. Declare imports
|
||||||
|
with `@importFrom` in the roxygen block; never hand-edit `NAMESPACE`. Every exported
|
||||||
|
function needs `@param` for each argument and `@return`; exported API needs
|
||||||
|
`@examples`. Re-run roxygen in the same commit as the code change — compass files
|
||||||
|
documentation debt when code moves and its `man/` pages do not.
|
||||||
|
|
||||||
|
**Output.** User-facing progress goes through `message()` so callers can suppress it.
|
||||||
|
No `cat()` or `print()` in package code.
|
||||||
|
|
||||||
|
**Assets.** Nothing personal or client-identifying is vendored into `inst/` — brand
|
||||||
|
assets only. Version 0.3.2 removed headshots for exactly this reason.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
testthat edition 3, one file per source file (`tests/testthat/test_theme.R` etc.).
|
||||||
|
Tests are self-sufficient: no reliance on state left by an earlier test. New
|
||||||
|
behaviour needs a test; a bug fix needs a test that fails without the fix.
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
make check # R CMD check --no-manual against a built tarball
|
||||||
|
Rscript -e 'devtools::test()'
|
||||||
|
```
|
||||||
|
|
||||||
|
CI runs `R CMD check` on `rocker/r-ver:4.6` via `.gitea/workflows/check.yaml` for
|
||||||
|
every push and PR to `master`.
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
Bump `Version` in `DESCRIPTION` and add a `NEWS.md` entry under **New features**,
|
||||||
|
**Bug fixes**, or **Internal**. `NEWS.md` is the changelog; compass tracks the work
|
||||||
|
that led to it, not the release itself.
|
||||||
@@ -1,24 +1,40 @@
|
|||||||
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.3.1
|
||||||
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;
|
||||||
|
logo composition utilities; Quarto themes for HTML reports, PDF, Typst, and
|
||||||
|
Reveal.js presentations; and data-wrangling helpers for public-sector
|
||||||
|
analysis.
|
||||||
|
License: LGPL (>= 3)
|
||||||
|
URL: https://gitea.civilytics.org/Civilytics/civilyticsR
|
||||||
|
BugReports: https://gitea.civilytics.org/Civilytics/civilyticsR/issues
|
||||||
Depends:
|
Depends:
|
||||||
R (>= 2.15.1)
|
R (>= 4.1.0)
|
||||||
Imports:
|
Imports:
|
||||||
ggplot2,
|
ggplot2 (>= 4.0.0),
|
||||||
jpeg,
|
jpeg,
|
||||||
|
jsonlite,
|
||||||
png,
|
png,
|
||||||
stringr,
|
stringr,
|
||||||
gridExtra,
|
gridExtra,
|
||||||
grid
|
grid,
|
||||||
|
stringdist,
|
||||||
|
showtext,
|
||||||
|
sysfonts
|
||||||
Encoding: UTF-8
|
Encoding: UTF-8
|
||||||
LazyData: true
|
|
||||||
Suggests:
|
Suggests:
|
||||||
testthat
|
testthat (>= 3.0.0),
|
||||||
RoxygenNote: 7.2.1
|
tidycensus,
|
||||||
|
quarto,
|
||||||
|
flextable,
|
||||||
|
officer,
|
||||||
|
ragg
|
||||||
|
Config/testthat/edition: 3
|
||||||
|
Config/roxygen2/version: 8.0.0
|
||||||
|
RoxygenNote: 7.3.3
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
openjdk-11-jdk \
|
openjdk-11-jdk \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libssh2-1-dev \
|
libssh2-1-dev \
|
||||||
|
libudunits2-dev \
|
||||||
|
libgdal-dev \
|
||||||
|
libgeos-dev \
|
||||||
|
libproj-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /var/lib/shiny-server/bookmarks/shiny
|
&& mkdir -p /var/lib/shiny-server/bookmarks/shiny
|
||||||
|
|
||||||
RUN install2.r ggplot2 jpeg png stringr gridExtra grid testthat
|
|
||||||
|
RUN install2.r ggplot2 jpeg png stringr gridExtra grid testthat covr tidycensus stringdist
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
pipeline {
|
|
||||||
agent {
|
|
||||||
dockerfile true
|
|
||||||
}
|
|
||||||
stages {
|
|
||||||
stage('Docker setup') {
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
R --version
|
|
||||||
java --version
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
stage('Build and test') {
|
|
||||||
stages {
|
|
||||||
stage("Build package") {
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
R CMD build .
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Check') {
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
R CMD check --no-manual civilytics_0.1.0.tar.gz
|
|
||||||
'''
|
|
||||||
|
|
||||||
sh '''
|
|
||||||
R CMD INSTALL civilytics_0.1.0.tar.gz
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('testthat'){
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
R -e 'testthat::test_local(".")'
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Clean') {
|
|
||||||
steps {
|
|
||||||
|
|
||||||
sh '''
|
|
||||||
@rm -rf civilytics_0.1.0.tar.gz civilytics.Rcheck
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
GNU Lesser General Public License
|
||||||
|
=================================
|
||||||
|
|
||||||
|
_Version 3, 29 June 2007_
|
||||||
|
_Copyright © 2007 Free Software Foundation, Inc. <<http://fsf.org/>>_
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -1,34 +1,82 @@
|
|||||||
# Generated by roxygen2: do not edit by hand
|
# Generated by roxygen2: do not edit by hand
|
||||||
|
|
||||||
|
export(beep)
|
||||||
export(add_logo)
|
export(add_logo)
|
||||||
export(add_logo_ga)
|
export(add_logo_ga)
|
||||||
|
export(agresti_coull_interval)
|
||||||
|
export(civilytics_colors)
|
||||||
|
export(civilytics_load_fonts)
|
||||||
|
export(civilytics_logo)
|
||||||
|
export(civilytics_pal)
|
||||||
|
export(civilytics_palette)
|
||||||
|
export(civilytics_palettes)
|
||||||
|
export(clopper_pearson)
|
||||||
export(countCleanr)
|
export(countCleanr)
|
||||||
export(countDots)
|
export(countDots)
|
||||||
export(countNA)
|
export(countNA)
|
||||||
export(dbSafeNames)
|
export(dbSafeNames)
|
||||||
export(findDots)
|
export(findDots)
|
||||||
|
export(get_fips)
|
||||||
export(get_png)
|
export(get_png)
|
||||||
|
export(get_stabbr)
|
||||||
export(grade_level_to_num)
|
export(grade_level_to_num)
|
||||||
export(has_caption)
|
export(has_caption)
|
||||||
export(make_logo_grob)
|
export(make_logo_grob)
|
||||||
|
export(match_test)
|
||||||
export(measure_caption)
|
export(measure_caption)
|
||||||
|
export(na_sum)
|
||||||
export(na_zero)
|
export(na_zero)
|
||||||
export(nvals)
|
export(nvals)
|
||||||
|
export(outersect)
|
||||||
|
export(perturb_count)
|
||||||
export(plot_jpeg)
|
export(plot_jpeg)
|
||||||
|
export(postcode_lookup)
|
||||||
export(pretty_count)
|
export(pretty_count)
|
||||||
export(pretty_per)
|
export(pretty_per)
|
||||||
export(race_short_names)
|
export(race_short_names)
|
||||||
|
export(random_round)
|
||||||
|
export(rnh)
|
||||||
|
export(round_to_nearest_half)
|
||||||
export(safe_max)
|
export(safe_max)
|
||||||
|
export(safe_ratio)
|
||||||
|
export(save_branded_flextable_png)
|
||||||
|
export(scale_color_civilytics)
|
||||||
|
export(scale_fill_civilytics)
|
||||||
export(simpleCap)
|
export(simpleCap)
|
||||||
|
export(stamp_logo_png)
|
||||||
export(star_subs)
|
export(star_subs)
|
||||||
|
export(style_flextable_civilytics)
|
||||||
export(theme_civilytics)
|
export(theme_civilytics)
|
||||||
|
export(theme_civilytics_dark)
|
||||||
|
export(theme_civilytics_dark_map)
|
||||||
|
export(theme_civilytics_map)
|
||||||
|
export(theme_civilytics_slide)
|
||||||
|
export(theme_civilytics_slide_map)
|
||||||
|
export(trim_max)
|
||||||
|
export(use_civilytics_brand)
|
||||||
|
export(use_civilytics_revealjs)
|
||||||
|
export(use_civilytics_theme)
|
||||||
|
export(waldInterval)
|
||||||
|
export(z_gap_test)
|
||||||
|
export(z_univariate)
|
||||||
import(ggplot2)
|
import(ggplot2)
|
||||||
|
importFrom(ggplot2,annotation_custom)
|
||||||
|
importFrom(ggplot2,ggplot)
|
||||||
importFrom(ggplot2,theme)
|
importFrom(ggplot2,theme)
|
||||||
importFrom(graphics,plot)
|
importFrom(ggplot2,theme_void)
|
||||||
|
importFrom(grDevices,dev.off)
|
||||||
|
importFrom(grDevices,png)
|
||||||
importFrom(graphics,rasterImage)
|
importFrom(graphics,rasterImage)
|
||||||
importFrom(grid,grid.draw)
|
importFrom(grid,grid.draw)
|
||||||
|
importFrom(grid,grid.newpage)
|
||||||
|
importFrom(grid,grid.raster)
|
||||||
importFrom(grid,rasterGrob)
|
importFrom(grid,rasterGrob)
|
||||||
importFrom(gridExtra,arrangeGrob)
|
importFrom(gridExtra,arrangeGrob)
|
||||||
importFrom(jpeg,readJPEG)
|
importFrom(jpeg,readJPEG)
|
||||||
importFrom(png,readPNG)
|
importFrom(png,readPNG)
|
||||||
|
importFrom(stats,qbeta)
|
||||||
|
importFrom(stats,qnorm)
|
||||||
|
importFrom(stats,runif)
|
||||||
|
importFrom(stringdist,stringsim)
|
||||||
importFrom(stringr,str_count)
|
importFrom(stringr,str_count)
|
||||||
|
importFrom(utils,flush.console)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# civilytics 0.3.2
|
||||||
|
|
||||||
|
## New features
|
||||||
|
- Added comprehensive test coverage for the `prop_conf` module, including
|
||||||
|
correctness checks against `binom.test()` for Clopper-Pearson intervals and
|
||||||
|
formula-based verification for Wald and Agresti-Coull intervals.
|
||||||
|
|
||||||
|
## Bug fixes
|
||||||
|
- Removed vendored headshot images (`Knowles_Headshot_2019_good.jpg`,
|
||||||
|
`Knowles_Headshot_2019_prisma.jpg`) from `inst/img/` to prevent personal
|
||||||
|
photos from being distributed with the package. The `plot_jpeg()` example
|
||||||
|
now references a generic placeholder path instead of a specific headshot.
|
||||||
|
|
||||||
|
## Internal
|
||||||
|
- Renamed `LICENSE.md` to `LICENSE` for R packaging convention compliance so
|
||||||
|
that the LGPL-3 license ships correctly with built tarballs.
|
||||||
@@ -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"))
|
||||||
@@ -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
|
||||||
|
#' [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 [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) {
|
||||||
|
discrete_scale(
|
||||||
|
"colour",
|
||||||
|
palette = civilytics_pal(palette, reverse = reverse),
|
||||||
|
...
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
pal <- civilytics_palette(palette, reverse = reverse)
|
||||||
|
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 [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) {
|
||||||
|
discrete_scale(
|
||||||
|
"fill",
|
||||||
|
palette = civilytics_pal(palette, reverse = reverse),
|
||||||
|
...
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
pal <- civilytics_palette(palette, reverse = reverse)
|
||||||
|
scale_fill_gradientn(
|
||||||
|
colours = grDevices::colorRampPalette(pal)(256),
|
||||||
|
...
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
#' @return a numeric column of data
|
#' @return a numeric column of data
|
||||||
#' @export
|
#' @export
|
||||||
countCleanr <- function(x){
|
countCleanr <- function(x){
|
||||||
if(class(x) == "character"){
|
if ("character" %in% class(x)) {
|
||||||
x[x == "None not reported"] <- "0"
|
x[x == "None not reported"] <- "0"
|
||||||
x[x == "Not applicable"] <- NA
|
x[x == "Not applicable"] <- NA
|
||||||
x <- as.numeric(x)
|
x <- as.numeric(x)
|
||||||
@@ -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)
|
||||||
@@ -115,8 +110,14 @@ nvals <- function(x){
|
|||||||
#' my_string <- c("Happy school", "Easy school", "cool School", "big school")
|
#' my_string <- c("Happy school", "Easy school", "cool School", "big school")
|
||||||
#' simpleCap(my_string)
|
#' simpleCap(my_string)
|
||||||
simpleCap <- function(x) {
|
simpleCap <- function(x) {
|
||||||
stopifnot(class(x) == "character")
|
stopifnot("character" %in% class(x))
|
||||||
s <- strsplit(x, " ")[[1]]
|
|
||||||
paste(toupper(substring(s, 1,1)), substring(s, 2),
|
# Vectorised over elements of x — each element is capitalised independently.
|
||||||
|
# unname() strips names inherited from the input vector so the output
|
||||||
|
# matches the original scalar behaviour (no names attribute).
|
||||||
|
unname(vapply(x, function(word) {
|
||||||
|
s <- strsplit(word, " ")[[1]]
|
||||||
|
paste(toupper(substring(s, 1, 1)), substring(s, 2),
|
||||||
sep = "", collapse = " ")
|
sep = "", collapse = " ")
|
||||||
|
}, character(1)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#' Apply the Civilytics brand styling to a flextable
|
||||||
|
#'
|
||||||
|
#' Applies *only* the visual Civilytics brand to an already-structured
|
||||||
|
#' [flextable::flextable()] — header fill and color, body font and size,
|
||||||
|
#' zebra striping, borders, footer styling, and a fixed table layout. The
|
||||||
|
#' caller remains responsible for table *structure*: labels
|
||||||
|
#' ([flextable::set_header_labels()]), header/title lines
|
||||||
|
#' ([flextable::add_header_lines()]), column widths ([flextable::width()]),
|
||||||
|
#' alignment ([flextable::align()]), and footer text
|
||||||
|
#' ([flextable::add_footer_lines()]). This separation keeps styling reusable
|
||||||
|
#' across projects while leaving content decisions where they belong.
|
||||||
|
#'
|
||||||
|
#' `flextable`, `officer`, and `ragg` are Suggested (not Imported) to keep the
|
||||||
|
#' base install light, so this function errors with an install hint if
|
||||||
|
#' `flextable` is unavailable.
|
||||||
|
#'
|
||||||
|
#' @param ft A [flextable::flextable()] object.
|
||||||
|
#' @param header_bg Character. Header background fill. Default `"#2c3e50"`.
|
||||||
|
#' @param header_color Character. Header text color. Default `"white"`.
|
||||||
|
#' @param title_fontsize Numeric. Font size for the first header line (the
|
||||||
|
#' title row, `i = 1`). Default `14`.
|
||||||
|
#' @param body_fontsize Numeric. Body font size. Default `11`.
|
||||||
|
#' @param font_name Character. Font family applied to all parts. Default
|
||||||
|
#' `"Arial"`.
|
||||||
|
#' @param zebra Logical. Apply alternating-row striping to even body rows.
|
||||||
|
#' Default `TRUE`. Safely skipped for tables with fewer than two body rows.
|
||||||
|
#' @param zebra_bg Character. Fill color for striped (even) body rows.
|
||||||
|
#' Default `"#f0f0eb"`.
|
||||||
|
#' @param outer_border_color Character. Outer border color. Default
|
||||||
|
#' `"#888888"`.
|
||||||
|
#' @param outer_border_width Numeric. Outer border width. Default `1`.
|
||||||
|
#' @param inner_border_color Character. Inner horizontal border color (body).
|
||||||
|
#' Default `"#cccccc"`.
|
||||||
|
#' @param inner_border_width Numeric. Inner horizontal border width. Default
|
||||||
|
#' `0.5`.
|
||||||
|
#' @param footer_fontsize Numeric. Footer font size (applied only if a footer
|
||||||
|
#' part exists). Default `9`.
|
||||||
|
#' @param footer_color Character. Footer text color. Default `"#555555"`.
|
||||||
|
#'
|
||||||
|
#' @return The styled [flextable::flextable()] object.
|
||||||
|
#' @export
|
||||||
|
#' @seealso [save_branded_flextable_png()] to export the styled table to a
|
||||||
|
#' logo-stamped PNG.
|
||||||
|
#' @examples
|
||||||
|
#' \dontrun{
|
||||||
|
#' library(flextable)
|
||||||
|
#' ft <- flextable(head(mtcars)) |>
|
||||||
|
#' add_header_lines("Motor Trend Cars") |>
|
||||||
|
#' add_footer_lines("Source: mtcars") |>
|
||||||
|
#' style_flextable_civilytics()
|
||||||
|
#' }
|
||||||
|
style_flextable_civilytics <- function(ft,
|
||||||
|
header_bg = "#2c3e50",
|
||||||
|
header_color = "white",
|
||||||
|
title_fontsize = 14,
|
||||||
|
body_fontsize = 11,
|
||||||
|
font_name = "Arial",
|
||||||
|
zebra = TRUE,
|
||||||
|
zebra_bg = "#f0f0eb",
|
||||||
|
outer_border_color = "#888888",
|
||||||
|
outer_border_width = 1,
|
||||||
|
inner_border_color = "#cccccc",
|
||||||
|
inner_border_width = 0.5,
|
||||||
|
footer_fontsize = 9,
|
||||||
|
footer_color = "#555555") {
|
||||||
|
if (!requireNamespace("flextable", quietly = TRUE)) {
|
||||||
|
stop("Install 'flextable' to use this function.", call. = FALSE)
|
||||||
|
}
|
||||||
|
if (!requireNamespace("officer", quietly = TRUE)) {
|
||||||
|
stop("Install 'officer' to use this function.", call. = FALSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Header: navy fill, white bold text, larger title line.
|
||||||
|
ft <- flextable::bg(ft, bg = header_bg, part = "header")
|
||||||
|
ft <- flextable::color(ft, color = header_color, part = "header")
|
||||||
|
ft <- flextable::bold(ft, part = "header")
|
||||||
|
ft <- flextable::fontsize(ft, i = 1, size = title_fontsize, part = "header")
|
||||||
|
|
||||||
|
# Body: readable size, brand font across all parts.
|
||||||
|
ft <- flextable::fontsize(ft, size = body_fontsize, part = "body")
|
||||||
|
ft <- flextable::font(ft, fontname = font_name, part = "all")
|
||||||
|
|
||||||
|
# Zebra striping on even body rows, guarded for tables with < 2 rows.
|
||||||
|
nrow_body <- flextable::nrow_part(ft, part = "body")
|
||||||
|
if (zebra && nrow_body >= 2) {
|
||||||
|
ft <- flextable::bg(ft, i = seq(2, nrow_body, 2), bg = zebra_bg,
|
||||||
|
part = "body")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Borders: outer frame on all parts, light horizontal rules in the body.
|
||||||
|
ft <- flextable::border_outer(
|
||||||
|
ft,
|
||||||
|
border = officer::fp_border(color = outer_border_color,
|
||||||
|
width = outer_border_width),
|
||||||
|
part = "all"
|
||||||
|
)
|
||||||
|
ft <- flextable::border_inner_h(
|
||||||
|
ft,
|
||||||
|
border = officer::fp_border(color = inner_border_color,
|
||||||
|
width = inner_border_width),
|
||||||
|
part = "body"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Footer styling — only if the table actually has a footer part.
|
||||||
|
if (flextable::nrow_part(ft, part = "footer") > 0) {
|
||||||
|
ft <- flextable::fontsize(ft, size = footer_fontsize, part = "footer")
|
||||||
|
ft <- flextable::color(ft, color = footer_color, part = "footer")
|
||||||
|
}
|
||||||
|
|
||||||
|
flextable::set_table_properties(ft, layout = "fixed")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#' Save a branded flextable to a logo-stamped PNG
|
||||||
|
#'
|
||||||
|
#' Exports a styled [flextable::flextable()] to a PNG via
|
||||||
|
#' [ragg::agg_png()] (sized to the table's own dimensions plus a little
|
||||||
|
#' headroom for the logo), then optionally stamps the Civilytics logo onto the
|
||||||
|
#' file with [stamp_logo_png()] so file-based tables stay visually consistent
|
||||||
|
#' with [civilytics_logo()]-branded plots.
|
||||||
|
#'
|
||||||
|
#' `ragg` and `flextable` are Suggested (not Imported); this function errors
|
||||||
|
#' with an install hint if either is unavailable.
|
||||||
|
#'
|
||||||
|
#' @param ft A [flextable::flextable()] object, typically already styled with
|
||||||
|
#' [style_flextable_civilytics()].
|
||||||
|
#' @param path Character. Output PNG path. Returned invisibly.
|
||||||
|
#' @param logo Logical. Stamp the Civilytics logo onto the saved PNG via
|
||||||
|
#' [stamp_logo_png()]. Default `TRUE`.
|
||||||
|
#' @param res Numeric. Output resolution in PPI passed to [ragg::agg_png()].
|
||||||
|
#' Default `300`.
|
||||||
|
#' @param extra_height Numeric. Additional height in inches added to the
|
||||||
|
#' table's natural height to leave room for the stamped logo. Default `0.4`.
|
||||||
|
#' @param ... Additional arguments forwarded to [stamp_logo_png()] (e.g.
|
||||||
|
#' `type`, `variant`, `position`, `width_frac`, `margin_frac`).
|
||||||
|
#'
|
||||||
|
#' @return `path`, invisibly.
|
||||||
|
#' @export
|
||||||
|
#' @seealso [style_flextable_civilytics()] to apply the brand styling, and
|
||||||
|
#' [stamp_logo_png()] for the underlying logo compositing.
|
||||||
|
#' @examples
|
||||||
|
#' \dontrun{
|
||||||
|
#' library(flextable)
|
||||||
|
#' ft <- flextable(head(mtcars)) |>
|
||||||
|
#' add_footer_lines("Source: mtcars") |>
|
||||||
|
#' style_flextable_civilytics()
|
||||||
|
#' save_branded_flextable_png(ft, "table.png")
|
||||||
|
#' save_branded_flextable_png(ft, "table.png", position = "bottom-left")
|
||||||
|
#' knitr::include_graphics("table.png")
|
||||||
|
#' }
|
||||||
|
save_branded_flextable_png <- function(ft, path, logo = TRUE, res = 300,
|
||||||
|
extra_height = 0.4, ...) {
|
||||||
|
if (!requireNamespace("flextable", quietly = TRUE)) {
|
||||||
|
stop("Install 'flextable' to use this function.", call. = FALSE)
|
||||||
|
}
|
||||||
|
if (!requireNamespace("ragg", quietly = TRUE)) {
|
||||||
|
stop("Install 'ragg' to use this function.", call. = FALSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
d <- flextable::flextable_dim(ft)
|
||||||
|
ragg::agg_png(path, width = d$width, height = d$height + extra_height,
|
||||||
|
units = "in", res = res)
|
||||||
|
on.exit(grDevices::dev.off(), add = TRUE)
|
||||||
|
plot(ft)
|
||||||
|
|
||||||
|
if (logo) {
|
||||||
|
# dev.off() must run before stamp_logo_png() re-reads the file. Flush the
|
||||||
|
# device now and clear the on.exit handler so it does not fire twice.
|
||||||
|
grDevices::dev.off()
|
||||||
|
on.exit()
|
||||||
|
stamp_logo_png(path, ...)
|
||||||
|
}
|
||||||
|
|
||||||
|
invisible(path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# Mutable package state held in an environment so the binding itself stays
|
||||||
|
# locked (R locks all namespace bindings at load time) while the contents
|
||||||
|
# remain writable. See https://adv-r.hadley.nz/environments.html#environments-as-containers
|
||||||
|
.cv_state <- new.env(parent = emptyenv())
|
||||||
|
.cv_state$fonts_loaded <- FALSE
|
||||||
|
|
||||||
|
#' Load Civilytics brand fonts
|
||||||
|
#'
|
||||||
|
#' Downloads Inter, Libre Franklin, Source Serif 4 and JetBrains Mono 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).
|
||||||
|
#'
|
||||||
|
#' Subsequent calls within the same session are no-ops unless `force = TRUE`.
|
||||||
|
#'
|
||||||
|
#' @param force Logical. If `TRUE`, reload fonts even if they were already
|
||||||
|
#' loaded in this session. Default `FALSE`.
|
||||||
|
#'
|
||||||
|
#' @return Invisibly returns `TRUE` if fonts were loaded, `FALSE` if skipped
|
||||||
|
#' (already loaded and `force = FALSE`).
|
||||||
|
#' @export
|
||||||
|
#'
|
||||||
|
#' @examples
|
||||||
|
#' \dontrun{
|
||||||
|
#' civilytics_load_fonts()
|
||||||
|
#' }
|
||||||
|
civilytics_load_fonts <- function(force = FALSE) {
|
||||||
|
if (.cv_state$fonts_loaded && !force) return(invisible(FALSE))
|
||||||
|
|
||||||
|
sysfonts::font_add_google("Inter", family = "Inter")
|
||||||
|
sysfonts::font_add_google("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_state$fonts_loaded <- TRUE
|
||||||
|
invisible(TRUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
.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."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 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 look for a match in.
|
||||||
|
#' @param distinct Logical. Should duplicate values of `x` and `y` be removed
|
||||||
|
#' before testing? Default is `TRUE`.
|
||||||
|
#'
|
||||||
|
#' @return Invisibly returns `NULL`; prints a formatted summary of match
|
||||||
|
#' statistics to the console via [writeLines()].
|
||||||
|
#' @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)
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: DO not report 100% if there is even 1 mismatch
|
||||||
|
|
||||||
|
xiny <- sum(x %in% y)
|
||||||
|
total_x <- length(x)
|
||||||
|
pct_x <- round(100 * xiny / total_x, 2)
|
||||||
|
|
||||||
|
yinx <- sum(y %in% x)
|
||||||
|
total_y <- length(y)
|
||||||
|
pct_y <- round(100 * yinx / total_y, 2)
|
||||||
|
|
||||||
|
header <- if (distinct) "Distinct Matches" else "All Values"
|
||||||
|
|
||||||
|
lines <- c(
|
||||||
|
paste0("**** ", header, " ****"),
|
||||||
|
"",
|
||||||
|
"X in Y",
|
||||||
|
sprintf("Of the %d X values, %d (%s%%) were matched.",
|
||||||
|
total_x, xiny, format(pct_x, nsmall = 2)),
|
||||||
|
strrep("*", 40),
|
||||||
|
"",
|
||||||
|
"Y in X",
|
||||||
|
sprintf("Of the %d Y values, %d (%s%%) were matched.",
|
||||||
|
total_y, yinx, format(pct_y, nsmall = 2)),
|
||||||
|
strrep("*", 38)
|
||||||
|
)
|
||||||
|
|
||||||
|
writeLines(lines)
|
||||||
|
invisible(NULL)
|
||||||
|
}
|
||||||
@@ -9,11 +9,14 @@
|
|||||||
#' @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{
|
||||||
|
#' # Supply a path to your own JPEG file
|
||||||
|
#' img <- "my_photo.jpg"
|
||||||
#' 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 +33,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,19 +47,51 @@ 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")) {
|
||||||
|
position <- match.arg(position)
|
||||||
|
at_top <- grepl("top", position)
|
||||||
|
|
||||||
|
# Capture the plot's background color so we can fill the entire composed
|
||||||
|
# grob with it. The logo grob must stay transparent (theme_void) so that
|
||||||
|
# caption/axis text overlapping via negative margins remains visible.
|
||||||
|
bg_fill <- plot$theme$plot.background$fill
|
||||||
|
|
||||||
|
# Inflate text sizes to compensate for arrangeGrob viewport shrinkage.
|
||||||
|
# All theme text elements use rel() sizing, so scaling the root 'text'
|
||||||
|
# element cascades to titles, axis labels, legends, captions, and strips.
|
||||||
|
if (!is.null(font_scale) && font_scale != 1) {
|
||||||
|
base_size <- plot$theme$text$size %||% 14
|
||||||
|
plot <- plot + theme(
|
||||||
|
text = element_text(size = base_size * font_scale)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (at_top) {
|
||||||
|
# For top placement, tighten the top margin of the plot
|
||||||
|
if (!is.null(margin_param)) {
|
||||||
|
plot <- plot + theme(plot.margin = unit(c(margin_param, 7, 7, 7), "pt"))
|
||||||
|
} else {
|
||||||
|
plot <- plot + theme(plot.margin = unit(c(-14, 7, 7, 7), "pt"))
|
||||||
|
}
|
||||||
|
composed <- arrangeGrob(logo, plot, heights = c(0.1, 0.93),
|
||||||
|
padding = unit(0.1, "line"))
|
||||||
|
} else {
|
||||||
|
# Bottom placement (original behaviour)
|
||||||
if(has_caption(plot)) {
|
if(has_caption(plot)) {
|
||||||
# convert the caption size to a negative number and on the "pt" scale
|
|
||||||
# p1$theme$plot.caption$size * 1.1
|
|
||||||
# Count the number of lines, which is this + 1
|
|
||||||
# For each line, we can add a certain negative space to align the logo
|
|
||||||
cap_lines <- measure_caption(plot)
|
cap_lines <- measure_caption(plot)
|
||||||
if (!is.null(margin_param)) {
|
if (!is.null(margin_param)) {
|
||||||
plot <- plot + theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt"))
|
plot <- plot + theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt"))
|
||||||
@@ -69,9 +101,19 @@ add_logo <- function(plot, logo, margin_param = NULL) {
|
|||||||
} else {
|
} else {
|
||||||
plot <- plot + theme(plot.margin = unit(c(7, 7, -14, 7), "pt"))
|
plot <- plot + theme(plot.margin = unit(c(7, 7, -14, 7), "pt"))
|
||||||
}
|
}
|
||||||
|
composed <- arrangeGrob(plot, logo, heights = c(0.93, 0.1),
|
||||||
arrangeGrob(plot, logo, heights = c(0.93, 0.1),
|
|
||||||
padding = unit(0.1, "line"))
|
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 +126,8 @@ add_logo <- function(plot, logo, margin_param = NULL) {
|
|||||||
#' @export
|
#' @export
|
||||||
#'
|
#'
|
||||||
#' @examples
|
#' @examples
|
||||||
#' p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
|
#' p1 <- ggplot(mtcars, aes(mpg, wt)) + 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 +146,23 @@ measure_caption <- function(gg) {
|
|||||||
#' @export
|
#' @export
|
||||||
#'
|
#'
|
||||||
#' @examples
|
#' @examples
|
||||||
#' p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
|
#' p1 <- ggplot(mtcars, aes(mpg, wt)) + 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 +170,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 + theme(
|
||||||
|
text = 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 +210,244 @@ 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"
|
||||||
|
make_logo_grob <- function(type = c("wordmark", "mark"),
|
||||||
|
variant = c("light", "dark"),
|
||||||
|
position = c("bottom-right", "bottom-left",
|
||||||
|
"top-right", "top-left")) {
|
||||||
|
type <- match.arg(type)
|
||||||
|
variant <- match.arg(variant)
|
||||||
|
position <- match.arg(position)
|
||||||
|
|
||||||
|
img_file <- switch(
|
||||||
|
paste(type, variant, sep = "_"),
|
||||||
|
wordmark_light = "civilytics-wordmark.png",
|
||||||
|
wordmark_dark = "civilytics-wordmark-reverse.png",
|
||||||
|
mark_light = "civilytics-mark.png",
|
||||||
|
mark_dark = "civilytics-mark-reverse.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
align_left <- grepl("left", position)
|
||||||
|
|
||||||
|
if (align_left) {
|
||||||
|
xmin <- 0
|
||||||
|
xmax <- if (type == "mark") 0.07 else 0.35
|
||||||
|
} else {
|
||||||
|
xmin <- if (type == "mark") 0.93 else 0.65
|
||||||
|
xmax <- 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ggplot() +
|
||||||
theme_void() +
|
theme_void() +
|
||||||
annotation_custom(get_png(system.file("img", "civilytics_logo.png",
|
annotation_custom(
|
||||||
package="civilytics")), xmin= 0.7, xmax = 1)
|
get_png(system.file("img", img_file, package = "civilytics")),
|
||||||
logo_grob
|
xmin = xmin, xmax = xmax
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#' Add a Civilytics logo to a ggplot (pipe-friendly)
|
||||||
|
#'
|
||||||
|
#' A convenience wrapper that creates the logo grob and attaches it to a
|
||||||
|
#' corner of the plot in one call. Designed for use with the base pipe `|>`.
|
||||||
|
#'
|
||||||
|
#' **Important:** R's `|>` has *higher* precedence than `+`, so you must
|
||||||
|
#' wrap the ggplot chain in parentheses before piping:
|
||||||
|
#'
|
||||||
|
#' ```
|
||||||
|
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
#' geom_point() +
|
||||||
|
#' theme_civilytics()) |>
|
||||||
|
#' civilytics_logo()
|
||||||
|
#' ```
|
||||||
|
#'
|
||||||
|
#' @param plot A ggplot object.
|
||||||
|
#' @param type Character. `"wordmark"` (default) or `"mark"`. Passed to
|
||||||
|
#' [make_logo_grob()].
|
||||||
|
#' @param variant Character. `"light"` (default) or `"dark"`. Passed to
|
||||||
|
#' [make_logo_grob()].
|
||||||
|
#' @param position Character. Corner placement for the logo: `"bottom-right"`
|
||||||
|
#' (default), `"bottom-left"`, `"top-right"`, or `"top-left"`.
|
||||||
|
#' @param margin_param Numeric or `NULL`. Manual margin adjustment passed to
|
||||||
|
#' [add_logo()].
|
||||||
|
#' @param font_scale Numeric. Inflate text sizes by this factor to compensate
|
||||||
|
#' for viewport shrinkage when composing with [gridExtra::arrangeGrob()].
|
||||||
|
#' Default `1.1` (~10 \% inflation). Set to `1` to disable.
|
||||||
|
#'
|
||||||
|
#' @return A grob (from [gridExtra::arrangeGrob()]) ready to draw with
|
||||||
|
#' [grid::grid.draw()].
|
||||||
|
#' @export
|
||||||
|
#'
|
||||||
|
#' @examples
|
||||||
|
#' \dontrun{
|
||||||
|
#' library(ggplot2); library(grid)
|
||||||
|
#'
|
||||||
|
#' # Pipe usage — parentheses required around the ggplot chain
|
||||||
|
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
#' geom_point() +
|
||||||
|
#' theme_civilytics()) |>
|
||||||
|
#' civilytics_logo() |>
|
||||||
|
#' grid.draw()
|
||||||
|
#'
|
||||||
|
#' # Top-right placement
|
||||||
|
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
#' geom_point() +
|
||||||
|
#' theme_civilytics()) |>
|
||||||
|
#' civilytics_logo(position = "top-right") |>
|
||||||
|
#' grid.draw()
|
||||||
|
#'
|
||||||
|
#' # Bottom-left with mark
|
||||||
|
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
#' geom_point() +
|
||||||
|
#' theme_civilytics()) |>
|
||||||
|
#' civilytics_logo(type = "mark", position = "bottom-left") |>
|
||||||
|
#' grid.draw()
|
||||||
|
#'
|
||||||
|
#' # Dark theme with mark in top-left
|
||||||
|
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
#' geom_point() +
|
||||||
|
#' theme_civilytics_dark()) |>
|
||||||
|
#' civilytics_logo(variant = "dark", type = "mark",
|
||||||
|
#' position = "top-left") |>
|
||||||
|
#' grid.draw()
|
||||||
|
#' }
|
||||||
|
civilytics_logo <- function(plot,
|
||||||
|
type = c("wordmark", "mark"),
|
||||||
|
variant = c("light", "dark"),
|
||||||
|
position = c("bottom-right", "bottom-left",
|
||||||
|
"top-right", "top-left"),
|
||||||
|
margin_param = NULL,
|
||||||
|
font_scale = 1.1) {
|
||||||
|
position <- match.arg(position)
|
||||||
|
logo <- make_logo_grob(type = type, variant = variant, position = position)
|
||||||
|
add_logo(plot, logo, margin_param = margin_param, font_scale = font_scale,
|
||||||
|
position = position)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#' Stamp the Civilytics logo onto a saved raster (PNG) image
|
||||||
|
#'
|
||||||
|
#' The raster analogue of [civilytics_logo()] for outputs that are already
|
||||||
|
#' rendered to a file rather than held as a ggplot/grob — e.g. a `flextable`
|
||||||
|
#' exported to PNG, or any `grDevices::png()` / `ragg::agg_png()` output.
|
||||||
|
#' Resolves the *same* brand asset that [make_logo_grob()] uses, so file-based
|
||||||
|
#' tables stay visually consistent with logo-branded plots, and composites it
|
||||||
|
#' into a corner of the image. Pure base-graphics + grid + png — no new
|
||||||
|
#' package dependencies.
|
||||||
|
#'
|
||||||
|
#' @param path Character. Path to the PNG to stamp. The file is overwritten
|
||||||
|
#' in place at its original pixel dimensions.
|
||||||
|
#' @param type Character. `"wordmark"` (default) or `"mark"`. As in
|
||||||
|
#' [make_logo_grob()].
|
||||||
|
#' @param variant Character. `"light"` (default, dark logo for light
|
||||||
|
#' backgrounds) or `"dark"` (reverse logo for dark backgrounds).
|
||||||
|
#' @param position Character. Corner placement: `"bottom-right"` (default),
|
||||||
|
#' `"bottom-left"`, `"top-right"`, or `"top-left"`.
|
||||||
|
#' @param width_frac Numeric. Logo width as a fraction of the image width
|
||||||
|
#' (default `0.15`). Height follows from the logo's aspect ratio.
|
||||||
|
#' @param margin_frac Numeric. Padding from the edges as a fraction of the
|
||||||
|
#' image width (default `0.02`).
|
||||||
|
#'
|
||||||
|
#' @return `path`, invisibly.
|
||||||
|
#' @export
|
||||||
|
#' @importFrom png readPNG
|
||||||
|
#' @importFrom grid grid.newpage grid.raster
|
||||||
|
#' @importFrom grDevices png dev.off
|
||||||
|
#' @examples
|
||||||
|
#' \dontrun{
|
||||||
|
#' # Brand a table exported to PNG so it matches civilytics_logo()-branded plots
|
||||||
|
#' ragg::agg_png("table.png", width = 8, height = 4, units = "in", res = 200)
|
||||||
|
#' plot(flextable::flextable(head(mtcars)))
|
||||||
|
#' dev.off()
|
||||||
|
#' stamp_logo_png("table.png") # wordmark, bottom-right
|
||||||
|
#' stamp_logo_png("table.png", type = "mark", position = "bottom-left")
|
||||||
|
#' }
|
||||||
|
stamp_logo_png <- function(path,
|
||||||
|
type = c("wordmark", "mark"),
|
||||||
|
variant = c("light", "dark"),
|
||||||
|
position = c("bottom-right", "bottom-left",
|
||||||
|
"top-right", "top-left"),
|
||||||
|
width_frac = 0.15,
|
||||||
|
margin_frac = 0.02) {
|
||||||
|
type <- match.arg(type)
|
||||||
|
variant <- match.arg(variant)
|
||||||
|
position <- match.arg(position)
|
||||||
|
stopifnot(file.exists(path))
|
||||||
|
|
||||||
|
# Same asset selection as make_logo_grob() so files match branded plots.
|
||||||
|
img_file <- switch(
|
||||||
|
paste(type, variant, sep = "_"),
|
||||||
|
wordmark_light = "civilytics-wordmark.png",
|
||||||
|
wordmark_dark = "civilytics-wordmark-reverse.png",
|
||||||
|
mark_light = "civilytics-mark.png",
|
||||||
|
mark_dark = "civilytics-mark-reverse.png"
|
||||||
|
)
|
||||||
|
logo_path <- system.file("img", img_file, package = "civilytics")
|
||||||
|
if (!nzchar(logo_path)) {
|
||||||
|
stop("Civilytics logo asset not found in the 'civilytics' package: ", img_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
base_img <- png::readPNG(path) # height x width x channels, values in [0, 1]
|
||||||
|
logo_img <- png::readPNG(logo_path)
|
||||||
|
h <- dim(base_img)[1]
|
||||||
|
w <- dim(base_img)[2]
|
||||||
|
aspect <- dim(logo_img)[1] / dim(logo_img)[2] # logo height / width
|
||||||
|
|
||||||
|
# Sizes/margins are expressed relative to image WIDTH, then converted to the
|
||||||
|
# device's npc units (which scale with the viewport's own width and height).
|
||||||
|
lw <- width_frac
|
||||||
|
lh <- width_frac * aspect * (w / h)
|
||||||
|
mx <- margin_frac
|
||||||
|
my <- margin_frac * (w / h)
|
||||||
|
|
||||||
|
x <- if (grepl("right", position)) 1 - mx else mx
|
||||||
|
y <- if (grepl("top", position)) 1 - my else my
|
||||||
|
just <- c(if (grepl("right", position)) "right" else "left",
|
||||||
|
if (grepl("top", position)) "top" else "bottom")
|
||||||
|
|
||||||
|
grDevices::png(path, width = w, height = h, units = "px")
|
||||||
|
on.exit(grDevices::dev.off(), add = TRUE)
|
||||||
|
grid::grid.newpage()
|
||||||
|
grid::grid.raster(base_img, width = 1, height = 1, interpolate = FALSE)
|
||||||
|
grid::grid.raster(logo_img, x = x, y = y, width = lw, height = lh,
|
||||||
|
just = just, interpolate = TRUE)
|
||||||
|
invisible(path)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) != ""
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
#' Install Civilytics Quarto Themes and Templates
|
||||||
|
#'
|
||||||
|
#' Helper functions that copy Civilytics Quarto assets from the installed
|
||||||
|
#' package into your Quarto project directory. Logos are stored in a single
|
||||||
|
#' canonical location (`inst/img/`) and copied to the paths expected by each
|
||||||
|
#' template at install time.
|
||||||
|
#'
|
||||||
|
#' @name quarto-helpers
|
||||||
|
NULL
|
||||||
|
|
||||||
|
|
||||||
|
# -- internal utilities -------------------------------------------------------
|
||||||
|
|
||||||
|
#' Copy a package file to a project directory
|
||||||
|
#'
|
||||||
|
#' @param src Relative path within the installed package (under `inst/`).
|
||||||
|
#' @param dst Destination path relative to `path`.
|
||||||
|
#' @param path Project root.
|
||||||
|
#' @param force Overwrite existing files?
|
||||||
|
#' @return Invisible logical indicating success.
|
||||||
|
#' @keywords internal
|
||||||
|
.copy_pkg_file <- function(src, dst, path, force) {
|
||||||
|
from <- system.file(src, package = "civilytics", mustWork = TRUE)
|
||||||
|
to <- file.path(path, dst)
|
||||||
|
dir.create(dirname(to), recursive = TRUE, showWarnings = FALSE)
|
||||||
|
|
||||||
|
if (file.exists(to) && !force) {
|
||||||
|
message(" skip: ", dst, " (already exists; use force = TRUE to overwrite)")
|
||||||
|
return(invisible(FALSE))
|
||||||
|
}
|
||||||
|
|
||||||
|
file.copy(from, to, overwrite = force)
|
||||||
|
message(" copy: ", dst)
|
||||||
|
invisible(TRUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
#' Copy logo SVGs from inst/img/ to a target directory
|
||||||
|
#'
|
||||||
|
#' @param dest_dir Destination directory relative to `path`.
|
||||||
|
#' @param path Project root.
|
||||||
|
#' @param force Overwrite existing files?
|
||||||
|
#' @param files Character vector of logo filenames to copy.
|
||||||
|
#' @keywords internal
|
||||||
|
.copy_logos <- function(dest_dir, path, force,
|
||||||
|
files = c("civilytics-mark.svg",
|
||||||
|
"civilytics-mark-reverse.svg",
|
||||||
|
"civilytics-wordmark.svg",
|
||||||
|
"civilytics-wordmark-reverse.svg",
|
||||||
|
"civilytics-pulse.svg")) {
|
||||||
|
for (f in files) {
|
||||||
|
.copy_pkg_file(file.path("img", f), file.path(dest_dir, f), path, force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -- public API ----------------------------------------------------------------
|
||||||
|
|
||||||
|
#' Install the Civilytics Reveal.js extension
|
||||||
|
#'
|
||||||
|
#' Copies the Civilytics Reveal.js Quarto extension into
|
||||||
|
#' `_extensions/civilytics-reveal/` under `path`, including brand logos
|
||||||
|
#' from the package. After running this function, add
|
||||||
|
#' `format: civilytics-reveal-revealjs` to your `.qmd` YAML front-matter.
|
||||||
|
#'
|
||||||
|
#' @param path Character. Project directory to install into. Default `"."`.
|
||||||
|
#' @param force Logical. Overwrite existing files? Default `FALSE`.
|
||||||
|
#'
|
||||||
|
#' @return Invisible `NULL`.
|
||||||
|
#' @export
|
||||||
|
#'
|
||||||
|
#' @examples
|
||||||
|
#' \dontrun{
|
||||||
|
#' use_civilytics_revealjs()
|
||||||
|
#' }
|
||||||
|
use_civilytics_revealjs <- function(path = ".", force = FALSE) {
|
||||||
|
path <- normalizePath(path, mustWork = TRUE)
|
||||||
|
message("Installing Civilytics Reveal.js extension into: ", path)
|
||||||
|
|
||||||
|
ext_src <- "quarto/extensions/civilytics-reveal"
|
||||||
|
ext_dst <- "_extensions/civilytics-reveal"
|
||||||
|
|
||||||
|
ext_files <- c(
|
||||||
|
"_extension.yml",
|
||||||
|
"civilytics.scss",
|
||||||
|
"civilytics.css",
|
||||||
|
"civilytics-head.html",
|
||||||
|
"civilytics-after.html"
|
||||||
|
)
|
||||||
|
for (f in ext_files) {
|
||||||
|
.copy_pkg_file(file.path(ext_src, f), file.path(ext_dst, f), path, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logos — single source from inst/img/
|
||||||
|
.copy_logos(file.path(ext_dst, "assets"), path, force)
|
||||||
|
|
||||||
|
message("\nDone! Add this to your .qmd front-matter:\n")
|
||||||
|
message("---")
|
||||||
|
message("format: civilytics-reveal-revealjs")
|
||||||
|
message("---")
|
||||||
|
invisible(NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#' Install the Civilytics document theme
|
||||||
|
#'
|
||||||
|
#' Copies Civilytics HTML, PDF (LaTeX), and Typst theme files into
|
||||||
|
#' your Quarto project. Also installs `_brand.yml` and the logo
|
||||||
|
#' assets it references.
|
||||||
|
#'
|
||||||
|
#' @inheritParams use_civilytics_revealjs
|
||||||
|
#'
|
||||||
|
#' @return Invisible `NULL`.
|
||||||
|
#' @export
|
||||||
|
#'
|
||||||
|
#' @examples
|
||||||
|
#' \dontrun{
|
||||||
|
#' use_civilytics_theme()
|
||||||
|
#' }
|
||||||
|
use_civilytics_theme <- function(path = ".", force = FALSE) {
|
||||||
|
path <- normalizePath(path, mustWork = TRUE)
|
||||||
|
message("Installing Civilytics document theme into: ", path)
|
||||||
|
|
||||||
|
# Brand file
|
||||||
|
.copy_pkg_file("quarto/_brand.yml", "_brand.yml", path, force)
|
||||||
|
|
||||||
|
# HTML theme
|
||||||
|
theme_files <- c("civilytics.scss", "_tokens.scss", "extras.css")
|
||||||
|
for (f in theme_files) {
|
||||||
|
.copy_pkg_file(file.path("quarto/theme", f), file.path("theme", f), path, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
# LaTeX
|
||||||
|
latex_files <- c("civilytics.tex", "civilytics-title.tex")
|
||||||
|
for (f in latex_files) {
|
||||||
|
.copy_pkg_file(file.path("quarto/latex", f), file.path("latex", f), path, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Typst — shipped as template-partials so Quarto keeps its Skylighting
|
||||||
|
# definitions and syntax-highlighted code blocks render (see issue #12)
|
||||||
|
typst_files <- c("typst-template.typ", "typst-show.typ")
|
||||||
|
for (f in typst_files) {
|
||||||
|
.copy_pkg_file(file.path("quarto/typst", f), file.path("typst", f), path, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logos — for _brand.yml (expects assets/logo/)
|
||||||
|
.copy_logos("assets/logo", path, force)
|
||||||
|
|
||||||
|
# Example report
|
||||||
|
.copy_pkg_file("quarto/examples/report.qmd", "examples/report.qmd", path, force)
|
||||||
|
|
||||||
|
message("\nDone! Example YAML for a report:\n")
|
||||||
|
message("---")
|
||||||
|
message("format:")
|
||||||
|
message(" html:")
|
||||||
|
message(" theme: theme/civilytics.scss")
|
||||||
|
message(" css: theme/extras.css")
|
||||||
|
message(" pdf:")
|
||||||
|
message(" include-in-header: latex/civilytics.tex")
|
||||||
|
message(" include-before-body: latex/civilytics-title.tex")
|
||||||
|
message(" typst:")
|
||||||
|
message(" template-partials:")
|
||||||
|
message(" - typst/typst-template.typ")
|
||||||
|
message(" - typst/typst-show.typ")
|
||||||
|
message("---")
|
||||||
|
message("\nSee examples/report.qmd for a complete example.")
|
||||||
|
invisible(NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#' Install the Civilytics brand file only
|
||||||
|
#'
|
||||||
|
#' Copies `_brand.yml` and the logo assets it references into your
|
||||||
|
#' Quarto project. This gives you Quarto 1.5+ automatic brand
|
||||||
|
#' styling (colors, fonts, logos) without the full theme SCSS.
|
||||||
|
#'
|
||||||
|
#' @inheritParams use_civilytics_revealjs
|
||||||
|
#'
|
||||||
|
#' @return Invisible `NULL`.
|
||||||
|
#' @export
|
||||||
|
#'
|
||||||
|
#' @examples
|
||||||
|
#' \dontrun{
|
||||||
|
#' use_civilytics_brand()
|
||||||
|
#' }
|
||||||
|
use_civilytics_brand <- function(path = ".", force = FALSE) {
|
||||||
|
path <- normalizePath(path, mustWork = TRUE)
|
||||||
|
message("Installing Civilytics brand file into: ", path)
|
||||||
|
|
||||||
|
.copy_pkg_file("quarto/_brand.yml", "_brand.yml", path, force)
|
||||||
|
.copy_logos("assets/logo", path, force)
|
||||||
|
|
||||||
|
message("\nDone! Quarto 1.5+ will auto-apply brand colors, fonts, and logos.")
|
||||||
|
message("See: https://quarto.org/docs/authoring/brand.html")
|
||||||
|
invisible(NULL)
|
||||||
|
}
|
||||||
@@ -1,41 +1,147 @@
|
|||||||
#' 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 [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.
|
||||||
|
#'
|
||||||
|
#' Brand fonts (Inter for UI text, Libre Franklin for titles) are loaded
|
||||||
|
#' automatically via [showtext] when the package is attached. Call
|
||||||
|
#' [civilytics_load_fonts()] to reload them if needed.
|
||||||
|
#'
|
||||||
|
#' @param font_size Numeric. Base font size in points. Default `14`.
|
||||||
|
#' @param font_family Character. Font family for body/axis text. Default
|
||||||
|
#' `"Inter"` (loaded via showtext).
|
||||||
|
#' @param title_family Character. Font family for plot titles and strip labels.
|
||||||
|
#' Default `"Libre Franklin"` (loaded via showtext).
|
||||||
|
#' @param line_size Numeric. Base line width. Default `0.5`.
|
||||||
|
#' @param rel_small Numeric. Scale factor for small text relative to
|
||||||
|
#' `font_size`. Default `12/14`.
|
||||||
|
#' @param rel_tiny Numeric. Scale factor for tiny text relative to `font_size`.
|
||||||
|
#' Default `11/14`.
|
||||||
|
#' @param rel_large Numeric. Scale factor for large text (titles) relative to
|
||||||
|
#' `font_size`. Default `20/14` (~1.43x), matching the Civilytics editorial
|
||||||
|
#' design system.
|
||||||
|
#' @param ink Character. Hex code for foreground/text color. Defaults to
|
||||||
|
#' [civilytics_colors]`["ink"]` (`#0E1A2B`).
|
||||||
|
#' @param paper Character. Hex code for background color. Defaults to
|
||||||
|
#' [civilytics_colors]`["paper"]` (`#FAF7F2`).
|
||||||
|
#' @param accent Character. Hex code for accent/highlight color. Defaults to
|
||||||
|
#' [civilytics_colors]`["ember_600"]` (`#C25311`).
|
||||||
|
#' @param strip_color Character. Hex code for facet strip background. Defaults
|
||||||
|
#' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).
|
||||||
|
#' @param grid Character. Which major gridlines to draw: `"y"` (default,
|
||||||
|
#' horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.
|
||||||
|
#' @param paper_bg Logical. If `TRUE`, fill the plot and panel backgrounds
|
||||||
|
#' with the warm `paper` color (Civilytics cream). Default is `FALSE`
|
||||||
|
#' (transparent) so that figures composite cleanly onto any background.
|
||||||
|
#' Set to `TRUE` for the branded cream canvas. Note: a transparent device
|
||||||
|
#' background (e.g., `dev = "ragg_png"`, `dev.args = list(background =
|
||||||
|
#' "transparent")`) is also needed for fully-transparent PNG exports.
|
||||||
|
#'
|
||||||
|
#' @section Font size hierarchy:
|
||||||
|
#' All text sizes are derived from `font_size` using relative scale factors.
|
||||||
|
#' At the default `font_size = 14`:
|
||||||
|
#'
|
||||||
|
#' | Element | Scale factor | Default size |
|
||||||
|
#' |:--------|:-------------|:-------------|
|
||||||
|
#' | Plot title | `rel_large` (1.43x) | ~20 pt |
|
||||||
|
#' | Subtitle | 1.0x | 14 pt |
|
||||||
|
#' | Axis text (tick labels) | `rel_small` (0.86x) | ~12 pt |
|
||||||
|
#' | Axis titles | `rel_small` (0.86x) | ~12 pt |
|
||||||
|
#' | Legend text | `rel_small` (0.86x) | ~12 pt |
|
||||||
|
#' | Caption | `rel_tiny` (0.79x) | ~11 pt |
|
||||||
|
#' | Legend title | `rel_tiny` (0.79x) | ~11 pt |
|
||||||
|
#' | Strip text (facets) | `rel_small` (0.86x) | ~12 pt |
|
||||||
|
#'
|
||||||
|
#' To uniformly scale all text, change `font_size`. To adjust only the title
|
||||||
|
#' prominence, change `rel_large`. When using [civilytics_logo()] to add a
|
||||||
|
#' logo below the plot, pass `font_scale` to compensate for viewport
|
||||||
|
#' shrinkage.
|
||||||
|
#'
|
||||||
|
#' @return A complete ggplot2 [theme()] object.
|
||||||
#' @export
|
#' @export
|
||||||
theme_civilytics <-
|
#'
|
||||||
function (font_size = 14,
|
#' @examples
|
||||||
font_family = "",
|
#' \dontrun{
|
||||||
|
#' library(ggplot2)
|
||||||
|
#'
|
||||||
|
#' # Default — transparent background for embedding
|
||||||
|
#' ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
#' geom_point() +
|
||||||
|
#' theme_civilytics()
|
||||||
|
#'
|
||||||
|
#' # With both gridlines and brand colors
|
||||||
|
#' ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||||
|
#' geom_point() +
|
||||||
|
#' scale_color_civilytics() +
|
||||||
|
#' theme_civilytics(grid = "both")
|
||||||
|
#'
|
||||||
|
#' # Branded cream background (opt-in)
|
||||||
|
#' ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
#' geom_point() +
|
||||||
|
#' theme_civilytics(paper_bg = TRUE)
|
||||||
|
#'
|
||||||
|
#' # Larger text for poster or display
|
||||||
|
#' ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
#' geom_point() +
|
||||||
|
#' theme_civilytics(font_size = 18)
|
||||||
|
#' }
|
||||||
|
theme_civilytics <- function(
|
||||||
|
font_size = 14,
|
||||||
|
font_family = CV_FONT_SANS,
|
||||||
|
title_family = CV_FONT_DISPLAY,
|
||||||
line_size = 0.5,
|
line_size = 0.5,
|
||||||
rel_small = 12 / 14,
|
rel_small = 12 / 14,
|
||||||
rel_tiny = 11 / 14,
|
rel_tiny = 11 / 14,
|
||||||
rel_large = 16 / 14) {
|
rel_large = 20 / 14,
|
||||||
|
ink = unname(civilytics_colors["ink"]),
|
||||||
|
paper = unname(civilytics_colors["paper"]),
|
||||||
|
accent = unname(civilytics_colors["ember_600"]),
|
||||||
|
strip_color = unname(civilytics_colors["paper_2"]),
|
||||||
|
grid = c("y", "x", "both", "none"),
|
||||||
|
paper_bg = FALSE) {
|
||||||
|
|
||||||
|
grid <- match.arg(grid)
|
||||||
half_line <- font_size / 2
|
half_line <- font_size / 2
|
||||||
small_size <- rel_small * font_size
|
small_size <- rel_small * font_size
|
||||||
theme_grey(base_size = font_size, base_family = font_family) %+replace%
|
rule_color <- unname(civilytics_colors["rule"])
|
||||||
|
ink_2 <- unname(civilytics_colors["ink_2"])
|
||||||
|
ink_3 <- unname(civilytics_colors["ink_3"])
|
||||||
|
bg_color <- if (isTRUE(paper_bg)) paper else NA
|
||||||
|
|
||||||
|
# Grid line elements
|
||||||
|
grid_line <- element_line(color = rule_color, linewidth = 0.35)
|
||||||
|
no_line <- element_blank()
|
||||||
|
|
||||||
|
theme_grey(
|
||||||
|
base_size = font_size,
|
||||||
|
base_family = font_family,
|
||||||
|
ink = ink,
|
||||||
|
paper = paper,
|
||||||
|
accent = accent
|
||||||
|
) %+replace%
|
||||||
theme(
|
theme(
|
||||||
line = element_line(
|
line = element_line(
|
||||||
color = "black",
|
color = ink,
|
||||||
size = line_size,
|
linewidth = line_size,
|
||||||
linetype = 1,
|
linetype = 1,
|
||||||
lineend = "butt"
|
lineend = "butt"
|
||||||
),
|
),
|
||||||
rect = element_rect(
|
rect = element_rect(
|
||||||
fill = NA,
|
fill = NA,
|
||||||
color = NA,
|
color = NA,
|
||||||
size = line_size,
|
linewidth = line_size,
|
||||||
linetype = 1
|
linetype = 1
|
||||||
),
|
),
|
||||||
text = element_text(
|
text = element_text(
|
||||||
family = font_family,
|
family = font_family,
|
||||||
face = "plain",
|
face = "plain",
|
||||||
color = "black",
|
color = ink,
|
||||||
size = font_size,
|
size = font_size,
|
||||||
hjust = 0.5,
|
hjust = 0.5,
|
||||||
vjust = 0.5,
|
vjust = 0.5,
|
||||||
@@ -44,125 +150,475 @@ theme_civilytics <-
|
|||||||
margin = margin(),
|
margin = margin(),
|
||||||
debug = FALSE
|
debug = FALSE
|
||||||
),
|
),
|
||||||
axis.line = element_line(
|
# -- Axes --
|
||||||
color = "black",
|
axis.line = element_blank(),
|
||||||
size = line_size,
|
axis.line.x = element_line(
|
||||||
|
color = ink,
|
||||||
|
linewidth = 0.6,
|
||||||
lineend = "square"
|
lineend = "square"
|
||||||
),
|
),
|
||||||
axis.line.x = NULL,
|
axis.line.y = element_blank(),
|
||||||
axis.line.y = NULL,
|
axis.text = element_text(
|
||||||
axis.text = element_text(color = "black",
|
color = ink_2,
|
||||||
size = small_size),
|
size = rel(rel_small)
|
||||||
axis.text.x = element_text(margin = margin(t = small_size / 4),
|
),
|
||||||
vjust = 1),
|
axis.text.x = element_text(
|
||||||
axis.text.x.top = element_text(margin = margin(b = small_size / 4),
|
margin = margin(t = small_size / 4),
|
||||||
vjust = 0),
|
vjust = 1
|
||||||
axis.text.y = element_text(margin = margin(r = small_size / 4),
|
),
|
||||||
hjust = 1),
|
axis.text.x.top = element_text(
|
||||||
axis.text.y.right = element_text(margin = margin(l = small_size / 4),
|
margin = margin(b = small_size / 4),
|
||||||
hjust = 0),
|
vjust = 0
|
||||||
axis.ticks = element_line(color = "black",
|
),
|
||||||
size = line_size),
|
axis.text.y = element_text(
|
||||||
axis.ticks.length = unit(half_line / 2,
|
margin = margin(r = small_size / 4),
|
||||||
"pt"),
|
hjust = 1
|
||||||
axis.title.x = element_text(margin = margin(t = half_line / 2),
|
),
|
||||||
vjust = 1),
|
axis.text.y.right = element_text(
|
||||||
axis.title.x.top = element_text(margin = margin(b = half_line / 2),
|
margin = margin(l = small_size / 4),
|
||||||
vjust = 0),
|
hjust = 0
|
||||||
|
),
|
||||||
|
axis.ticks = element_line(
|
||||||
|
color = ink_3,
|
||||||
|
linewidth = 0.4
|
||||||
|
),
|
||||||
|
axis.ticks.length = unit(4, "pt"),
|
||||||
|
axis.title.x = element_text(
|
||||||
|
size = rel(rel_small),
|
||||||
|
color = ink_3,
|
||||||
|
margin = margin(t = 10),
|
||||||
|
vjust = 1
|
||||||
|
),
|
||||||
|
axis.title.x.top = element_text(
|
||||||
|
size = rel(rel_small),
|
||||||
|
color = ink_3,
|
||||||
|
margin = margin(b = half_line / 2),
|
||||||
|
vjust = 0
|
||||||
|
),
|
||||||
axis.title.y = element_text(
|
axis.title.y = element_text(
|
||||||
|
size = rel(rel_small),
|
||||||
|
color = ink_3,
|
||||||
angle = 90,
|
angle = 90,
|
||||||
margin = margin(r = half_line /
|
margin = margin(r = 10),
|
||||||
2),
|
|
||||||
vjust = 1
|
vjust = 1
|
||||||
),
|
),
|
||||||
axis.title.y.right = element_text(
|
axis.title.y.right = element_text(
|
||||||
|
size = rel(rel_small),
|
||||||
|
color = ink_3,
|
||||||
angle = -90,
|
angle = -90,
|
||||||
margin = margin(l = half_line / 2),
|
margin = margin(l = half_line / 2),
|
||||||
vjust = 0
|
vjust = 0
|
||||||
),
|
),
|
||||||
|
# -- Legend --
|
||||||
legend.background = element_blank(),
|
legend.background = element_blank(),
|
||||||
legend.spacing = unit(font_size, "pt"),
|
legend.spacing = unit(font_size, "pt"),
|
||||||
legend.spacing.x = NULL,
|
legend.spacing.x = NULL,
|
||||||
legend.spacing.y = NULL,
|
legend.spacing.y = NULL,
|
||||||
legend.margin = margin(0,
|
legend.margin = margin(0, 0, 4, 0),
|
||||||
0, 0, 0),
|
|
||||||
legend.key = element_blank(),
|
legend.key = element_blank(),
|
||||||
legend.key.size = unit(1.1 *
|
legend.key.size = unit(12, "pt"),
|
||||||
font_size, "pt"),
|
|
||||||
legend.key.height = NULL,
|
legend.key.height = NULL,
|
||||||
legend.key.width = NULL,
|
legend.key.width = NULL,
|
||||||
legend.text = element_text(size = rel(rel_small)),
|
legend.text = element_text(
|
||||||
legend.text.align = NULL,
|
size = rel(rel_small),
|
||||||
legend.title = element_text(hjust = 0),
|
color = ink_2
|
||||||
legend.title.align = NULL,
|
),
|
||||||
legend.position = "right",
|
legend.title = element_text(
|
||||||
|
hjust = 0,
|
||||||
|
face = "bold",
|
||||||
|
size = rel(rel_tiny),
|
||||||
|
color = ink_3
|
||||||
|
),
|
||||||
|
legend.position = "top",
|
||||||
legend.direction = NULL,
|
legend.direction = NULL,
|
||||||
legend.justification = c("left",
|
legend.justification = c("left", "center"),
|
||||||
"center"),
|
|
||||||
legend.box = NULL,
|
legend.box = NULL,
|
||||||
legend.box.margin = margin(0,
|
legend.box.margin = margin(0, 0, 0, 0),
|
||||||
0, 0, 0),
|
|
||||||
legend.box.background = element_blank(),
|
legend.box.background = element_blank(),
|
||||||
legend.box.spacing = unit(font_size, "pt"),
|
legend.box.spacing = unit(font_size, "pt"),
|
||||||
panel.background = element_blank(),
|
# -- Panel --
|
||||||
|
panel.background = element_rect(fill = bg_color, color = NA),
|
||||||
panel.border = element_blank(),
|
panel.border = element_blank(),
|
||||||
panel.grid = element_blank(),
|
panel.grid.minor = element_blank(),
|
||||||
panel.grid.major = NULL,
|
panel.grid.major.x = if (grid %in% c("x", "both")) grid_line else no_line,
|
||||||
panel.grid.minor = NULL,
|
panel.grid.major.y = if (grid %in% c("y", "both")) grid_line else no_line,
|
||||||
panel.grid.major.x = NULL,
|
panel.spacing = unit(16, "pt"),
|
||||||
panel.grid.major.y = NULL,
|
|
||||||
panel.grid.minor.x = NULL,
|
|
||||||
panel.grid.minor.y = NULL,
|
|
||||||
panel.spacing = unit(half_line,
|
|
||||||
"pt"),
|
|
||||||
panel.spacing.x = NULL,
|
panel.spacing.x = NULL,
|
||||||
panel.spacing.y = NULL,
|
panel.spacing.y = NULL,
|
||||||
panel.ontop = FALSE,
|
panel.ontop = FALSE,
|
||||||
strip.background = element_rect(fill = "grey80"),
|
# -- Facet strips --
|
||||||
|
strip.background = element_rect(fill = strip_color, color = NA),
|
||||||
strip.text = element_text(
|
strip.text = element_text(
|
||||||
|
family = font_family,
|
||||||
|
face = "bold",
|
||||||
size = rel(rel_small),
|
size = rel(rel_small),
|
||||||
margin = margin(half_line / 2, half_line /
|
color = ink,
|
||||||
2, half_line / 2,
|
margin = margin(
|
||||||
half_line / 2)
|
half_line / 2, half_line / 2,
|
||||||
|
half_line / 2, half_line / 2
|
||||||
|
)
|
||||||
),
|
),
|
||||||
strip.text.x = NULL,
|
strip.text.x = NULL,
|
||||||
strip.text.y = element_text(angle = -90),
|
strip.text.y = element_text(angle = -90),
|
||||||
strip.placement = "inside",
|
strip.placement = "inside",
|
||||||
strip.placement.x = NULL,
|
strip.placement.x = NULL,
|
||||||
strip.placement.y = NULL,
|
strip.placement.y = NULL,
|
||||||
strip.switch.pad.grid = unit(half_line / 2,
|
strip.switch.pad.grid = unit(half_line / 2, "pt"),
|
||||||
"pt"),
|
strip.switch.pad.wrap = unit(half_line / 2, "pt"),
|
||||||
strip.switch.pad.wrap = unit(half_line / 2,
|
# -- Plot-level --
|
||||||
"pt"),
|
plot.background = element_rect(fill = bg_color, color = NA),
|
||||||
plot.background = element_blank(),
|
|
||||||
plot.title = element_text(
|
plot.title = element_text(
|
||||||
|
family = title_family,
|
||||||
face = "bold",
|
face = "bold",
|
||||||
size = rel(rel_large),
|
size = rel(rel_large),
|
||||||
hjust = 0,
|
hjust = 0,
|
||||||
vjust = 1,
|
vjust = 1,
|
||||||
margin = margin(b = half_line)
|
margin = margin(b = 4)
|
||||||
),
|
),
|
||||||
|
plot.title.position = "plot",
|
||||||
plot.subtitle = element_text(
|
plot.subtitle = element_text(
|
||||||
size = rel(rel_small),
|
size = rel(1),
|
||||||
|
color = ink_2,
|
||||||
hjust = 0,
|
hjust = 0,
|
||||||
vjust = 1,
|
vjust = 1,
|
||||||
margin = margin(b = half_line)
|
lineheight = 1.3,
|
||||||
|
margin = margin(b = 14)
|
||||||
),
|
),
|
||||||
plot.caption = element_text(
|
plot.caption = element_text(
|
||||||
size = rel(rel_tiny),
|
size = rel(rel_tiny),
|
||||||
hjust = 0, # set hjust to 0
|
color = ink_3,
|
||||||
|
hjust = 0,
|
||||||
vjust = 1,
|
vjust = 1,
|
||||||
lineheight = 1,
|
lineheight = 1.3,
|
||||||
margin = margin(t = half_line)
|
margin = margin(t = 14)
|
||||||
),
|
),
|
||||||
|
plot.caption.position = "plot",
|
||||||
plot.tag = element_text(
|
plot.tag = element_text(
|
||||||
face = "bold",
|
face = "bold",
|
||||||
|
color = accent,
|
||||||
|
size = rel(rel_tiny),
|
||||||
hjust = 0,
|
hjust = 0,
|
||||||
vjust = 0.7
|
vjust = 0.7
|
||||||
),
|
),
|
||||||
plot.tag.position = c(0, 1),
|
plot.tag.position = c(0, 1),
|
||||||
plot.margin = margin(half_line,
|
plot.margin = margin(16, 18, 16, 16),
|
||||||
half_line, half_line, half_line),
|
|
||||||
complete = TRUE
|
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 [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.
|
||||||
|
theme(
|
||||||
|
plot.subtitle = element_text(
|
||||||
|
color = unname(civilytics_colors["navy_200"])
|
||||||
|
),
|
||||||
|
plot.caption = element_text(
|
||||||
|
color = unname(civilytics_colors["navy_300"])
|
||||||
|
),
|
||||||
|
axis.text = element_text(
|
||||||
|
color = unname(civilytics_colors["navy_200"])
|
||||||
|
),
|
||||||
|
axis.title.x = element_text(
|
||||||
|
color = unname(civilytics_colors["navy_300"])
|
||||||
|
),
|
||||||
|
axis.title.y = 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 [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
|
||||||
|
) +
|
||||||
|
theme(
|
||||||
|
axis.line.x = element_line(
|
||||||
|
color = ink,
|
||||||
|
linewidth = 0.8,
|
||||||
|
lineend = "square"
|
||||||
|
),
|
||||||
|
plot.margin = 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 [theme()] object.
|
||||||
|
#' @keywords internal
|
||||||
|
.map_theme_extras <- function() {
|
||||||
|
theme(
|
||||||
|
axis.line = element_blank(),
|
||||||
|
axis.line.x = element_blank(),
|
||||||
|
axis.line.y = element_blank(),
|
||||||
|
axis.text = element_blank(),
|
||||||
|
axis.text.x = element_blank(),
|
||||||
|
axis.text.y = element_blank(),
|
||||||
|
axis.ticks = element_blank(),
|
||||||
|
axis.ticks.length = unit(0, "pt"),
|
||||||
|
axis.title.x = element_blank(),
|
||||||
|
axis.title.y = element_blank(),
|
||||||
|
panel.grid.major.x = element_blank(),
|
||||||
|
panel.grid.major.y = element_blank(),
|
||||||
|
panel.grid.minor = 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 [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 [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 [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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
utilities for public-sector analysis.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
You can install the released version of civilytics from [CRAN](https://CRAN.R-project.org) with:
|
Install from the Civilytics Gitea server:
|
||||||
|
|
||||||
``` r
|
``` r
|
||||||
install.packages("civilytics")
|
# install.packages("remotes")
|
||||||
|
remotes::install_git("https://gitea.civilytics.org/Civilytics/civilyticsR.git")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example
|
## Quick start
|
||||||
|
|
||||||
This is a basic example which shows you how to solve a common problem:
|
|
||||||
|
|
||||||
``` r
|
``` r
|
||||||
library(civilytics)
|
library(civilytics)
|
||||||
## basic example code
|
library(ggplot2)
|
||||||
|
|
||||||
|
ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||||
|
geom_point(size = 2.5) +
|
||||||
|
scale_color_civilytics() +
|
||||||
|
labs(
|
||||||
|
title = "Fuel economy by engine displacement",
|
||||||
|
subtitle = "Highway MPG vs. engine size for 234 vehicles",
|
||||||
|
caption = "Source: EPA fuel economy data (ggplot2::mpg)",
|
||||||
|
x = "Engine displacement (litres)",
|
||||||
|
y = "Highway MPG"
|
||||||
|
) +
|
||||||
|
theme_civilytics()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-quickstart-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
## Color palettes
|
||||||
|
|
||||||
|
The package ships 53 named brand colors in `civilytics_colors` and 10
|
||||||
|
curated palettes in `civilytics_palettes`. Use `civilytics_palette()` to
|
||||||
|
retrieve colors by name, or pass palettes directly to the ggplot2
|
||||||
|
scales.
|
||||||
|
|
||||||
|
### Palette gallery
|
||||||
|
|
||||||
|
<img src="man/figures/README-palette-gallery-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
### Using palettes
|
||||||
|
|
||||||
|
``` r
|
||||||
|
# Discrete fill with the qualitative palette
|
||||||
|
ggplot(mpg, aes(class, fill = class)) +
|
||||||
|
geom_bar(show.legend = FALSE) +
|
||||||
|
scale_fill_civilytics() +
|
||||||
|
labs(title = "Vehicle counts by class", x = NULL, y = NULL) +
|
||||||
|
theme_civilytics(grid = "y")
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-palette-usage-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
``` r
|
||||||
|
|
||||||
|
# Continuous fill with a sequential palette
|
||||||
|
ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
|
||||||
|
geom_tile() +
|
||||||
|
scale_fill_civilytics("seq_ember", discrete = FALSE) +
|
||||||
|
labs(title = "Old Faithful eruption density") +
|
||||||
|
theme_civilytics(grid = "none")
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-palette-usage-2.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
|
||||||
|
Three theme variants cover the most common output contexts. All share
|
||||||
|
the same typographic structure and accept `grid` and `paper_bg`
|
||||||
|
parameters.
|
||||||
|
|
||||||
|
### Editorial (default)
|
||||||
|
|
||||||
|
The default theme uses a warm paper background with horizontal gridlines
|
||||||
|
— an editorial, Pew-style layout.
|
||||||
|
|
||||||
|
``` r
|
||||||
|
base_plot <- ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
geom_point(aes(colour = factor(cyl)), size = 2) +
|
||||||
|
scale_color_civilytics() +
|
||||||
|
labs(
|
||||||
|
title = "Engine size vs. highway fuel economy",
|
||||||
|
subtitle = "Colored by number of cylinders",
|
||||||
|
caption = "Source: ggplot2::mpg",
|
||||||
|
colour = "Cylinders",
|
||||||
|
x = "Displacement (L)", y = "Highway MPG"
|
||||||
|
)
|
||||||
|
|
||||||
|
base_plot + theme_civilytics()
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-theme-editorial-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
### Grid options
|
||||||
|
|
||||||
|
The `grid` parameter controls which major gridlines are drawn.
|
||||||
|
|
||||||
|
<img src="man/figures/README-theme-grids-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
### Dark
|
||||||
|
|
||||||
|
Dark navy background with light text, suitable for presentations or
|
||||||
|
dashboards on dark surfaces.
|
||||||
|
|
||||||
|
``` r
|
||||||
|
base_plot + theme_civilytics_dark()
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-theme-dark-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
### Slide
|
||||||
|
|
||||||
|
Transparent background and larger base font (18 pt), sized for Reveal.js
|
||||||
|
slides or PowerPoint exports.
|
||||||
|
|
||||||
|
``` r
|
||||||
|
base_plot + theme_civilytics_slide()
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-theme-slide-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
### Facets
|
||||||
|
|
||||||
|
Facet strips use the `paper_2` tint, with the title font.
|
||||||
|
|
||||||
|
``` r
|
||||||
|
ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
geom_point(colour = civilytics_colors["navy_600"], size = 1.5) +
|
||||||
|
facet_wrap(~class, ncol = 4) +
|
||||||
|
labs(
|
||||||
|
title = "Highway MPG by vehicle class",
|
||||||
|
x = "Displacement (L)", y = "Highway MPG"
|
||||||
|
) +
|
||||||
|
theme_civilytics(grid = "y")
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-theme-facets-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
## Logo utilities
|
||||||
|
|
||||||
|
Add the Civilytics logo to any ggplot using the pipe-friendly
|
||||||
|
`civilytics_logo()` or the lower-level `add_logo()` /
|
||||||
|
`make_logo_grob()`. The logo is automatically right-aligned below the
|
||||||
|
plot area. Wrap the ggplot chain in parentheses before piping — R’s `|>`
|
||||||
|
binds tighter than `+`.
|
||||||
|
|
||||||
|
### Wordmark on a light plot
|
||||||
|
|
||||||
|
``` r
|
||||||
|
p <- ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||||
|
geom_point(size = 2) +
|
||||||
|
scale_color_civilytics() +
|
||||||
|
labs(
|
||||||
|
title = "Fuel economy by engine displacement",
|
||||||
|
caption = "Source: EPA fuel economy data (ggplot2::mpg)",
|
||||||
|
x = "Displacement (L)", y = "Highway MPG"
|
||||||
|
) +
|
||||||
|
theme_civilytics()
|
||||||
|
|
||||||
|
grid::grid.draw(civilytics_logo(p))
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-logo-wordmark-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
### Compact mark
|
||||||
|
|
||||||
|
Use `type = "mark"` for the compact C-pulse icon instead of the full
|
||||||
|
wordmark.
|
||||||
|
|
||||||
|
``` r
|
||||||
|
p2_logo <- ggplot(mpg, aes(displ, hwy, colour = factor(cyl))) +
|
||||||
|
geom_point(size = 2) +
|
||||||
|
scale_color_civilytics() +
|
||||||
|
labs(
|
||||||
|
title = "Engine size vs. highway fuel economy",
|
||||||
|
colour = "Cylinders",
|
||||||
|
x = "Displacement (L)", y = "Highway MPG"
|
||||||
|
) +
|
||||||
|
theme_civilytics()
|
||||||
|
|
||||||
|
grid::grid.draw(civilytics_logo(p2_logo, type = "mark"))
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-logo-mark-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
### Multi-plot layout with logo
|
||||||
|
|
||||||
|
Use `add_logo_ga()` to attach a single logo below a row of plots.
|
||||||
|
|
||||||
|
``` r
|
||||||
|
p1 <- ggplot(mpg, aes(class, fill = class)) +
|
||||||
|
geom_bar(show.legend = FALSE) +
|
||||||
|
scale_fill_civilytics() +
|
||||||
|
labs(title = "Vehicle counts", x = NULL, y = NULL) +
|
||||||
|
theme_civilytics(grid = "y")
|
||||||
|
|
||||||
|
p2 <- ggplot(mpg, aes(displ, hwy)) +
|
||||||
|
geom_point(colour = civilytics_colors["navy_600"], size = 1.5) +
|
||||||
|
labs(title = "Displacement vs. MPG", x = "Displacement (L)", y = "Highway MPG") +
|
||||||
|
theme_civilytics(grid = "y")
|
||||||
|
|
||||||
|
logo <- make_logo_grob()
|
||||||
|
grid::grid.draw(add_logo_ga(list(p1, p2), logo))
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="man/figures/README-logo-multi-1.png" alt="" width="100%" />
|
||||||
|
|
||||||
|
## Other utilities
|
||||||
|
|
||||||
|
The package also includes helpers for public-sector data analysis:
|
||||||
|
|
||||||
|
| Function | Purpose |
|
||||||
|
|:---|:---|
|
||||||
|
| `pretty_count()` / `pretty_per()` | Format numbers and percentages |
|
||||||
|
| `grade_level_to_num()` | Convert grade labels (KG, 01–12) to numeric |
|
||||||
|
| `race_short_names()` | Standardize NCES race/ethnicity categories |
|
||||||
|
| `get_fips()` / `get_stabbr()` | State FIPS code lookups |
|
||||||
|
| `clopper_pearson()` / `agresti_coull_interval()` | Proportion confidence intervals |
|
||||||
|
| `match_test()` / `trunc_match()` | Fuzzy join diagnostics |
|
||||||
|
| `perturb_count()` / `random_round()` | Privacy-preserving data perturbation |
|
||||||
|
|
||||||
|
## Maintaining brand assets
|
||||||
|
|
||||||
|
Logo and brand mark files live in `inst/img/`. The package ships both
|
||||||
|
PNG (for ggplot2 raster composition) and SVG (for Quarto/HTML output)
|
||||||
|
variants:
|
||||||
|
|
||||||
|
| File | Format | Used by |
|
||||||
|
|:---|:---|:---|
|
||||||
|
| `civilytics-wordmark.png` / `.svg` | Full “Civilytics” lockup | `make_logo_grob("wordmark", "light")`, Quarto templates |
|
||||||
|
| `civilytics-wordmark-reverse.png` / `.svg` | Light-on-dark wordmark | `make_logo_grob("wordmark", "dark")`, dark slides |
|
||||||
|
| `civilytics-mark.png` / `.svg` | Compact C-pulse icon | `make_logo_grob("mark", "light")` |
|
||||||
|
| `civilytics-mark-reverse.svg` | Light-on-dark mark | `make_logo_grob("mark", "dark")` |
|
||||||
|
| `civilytics-pulse.svg` | Standalone waveform glyph | Quarto slide footer chrome |
|
||||||
|
|
||||||
|
To update the logos, replace the files in `inst/img/` with new versions
|
||||||
|
using the same filenames. The PNG files must be raster images (the
|
||||||
|
ggplot2 logo functions read them via `png::readPNG()`). SVG files are
|
||||||
|
passed through as-is by Quarto and HTML templates.
|
||||||
|
|
||||||
|
After replacing files, re-render the README gallery to update the
|
||||||
|
screenshots:
|
||||||
|
|
||||||
|
``` r
|
||||||
|
devtools::load_all()
|
||||||
|
rmarkdown::render("README.Rmd")
|
||||||
|
```
|
||||||
|
|||||||
|
After Width: | Height: | Size: 682 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 149 KiB |
@@ -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 |
|
After Width: | Height: | Size: 20 KiB |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -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
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: "Who pays when rent outpaces wages?"
|
||||||
|
subtitle: "A 12-county analysis of cost-burdened renter households, 2019–2024."
|
||||||
|
author:
|
||||||
|
- name: "Jared Knowles"
|
||||||
|
affiliation: "Civilytics Consulting"
|
||||||
|
date: "2026-04-15"
|
||||||
|
abstract: |
|
||||||
|
We combine ACS 5-year microdata with HUD Fair Market Rent estimates
|
||||||
|
to track changes in rent burden across 12 metropolitan counties.
|
||||||
|
Between 2019 and 2024, the share of renter households spending more
|
||||||
|
than 30% of income on housing rose by 6.4 percentage points; the
|
||||||
|
increase was concentrated in counties where median wages stagnated.
|
||||||
|
categories: [Housing, Affordability, ACS]
|
||||||
|
format:
|
||||||
|
html:
|
||||||
|
theme: ../theme/civilytics.scss
|
||||||
|
css: ../theme/extras.css
|
||||||
|
toc: true
|
||||||
|
toc-location: right
|
||||||
|
typst:
|
||||||
|
template-partials:
|
||||||
|
- ../typst/typst-template.typ
|
||||||
|
- ../typst/typst-show.typ
|
||||||
|
pdf:
|
||||||
|
include-in-header: ../latex/civilytics.tex
|
||||||
|
include-before-body: ../latex/civilytics-title.tex
|
||||||
|
execute:
|
||||||
|
echo: false
|
||||||
|
warning: false
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings at a glance
|
||||||
|
|
||||||
|
::: {.stat-callout}
|
||||||
|
::: {}
|
||||||
|
[6.4 pp]{.stat-num}
|
||||||
|
[Increase in rent-burdened share, 2019→2024]{.stat-label .accent}
|
||||||
|
:::
|
||||||
|
::: {}
|
||||||
|
[1 in 2]{.stat-num}
|
||||||
|
[Renters in the studied counties now cost-burdened]{.stat-label}
|
||||||
|
:::
|
||||||
|
::: {}
|
||||||
|
[$412]{.stat-num}
|
||||||
|
[Median monthly rent gap, lowest-wage quintile]{.stat-label}
|
||||||
|
:::
|
||||||
|
:::
|
||||||
|
|
||||||
|
[Methodology note]{.eyebrow}
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Rent has outpaced wages in every county we studied. In some, the gap is
|
||||||
|
modest and recent; in others, it has been widening for a decade. This
|
||||||
|
report disaggregates the trend along three lines: county, income
|
||||||
|
quintile, and household composition.
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
#| label: fig-trend
|
||||||
|
#| fig-cap: "Cost-burdened renter share, 12 metro counties, 2019–2024."
|
||||||
|
#| fig-width: 7
|
||||||
|
#| fig-height: 4.2
|
||||||
|
library(ggplot2); library(civilytics)
|
||||||
|
ggplot(economics, aes(date, unemploy / pop)) +
|
||||||
|
geom_line(color = "#C25311", linewidth = 1) +
|
||||||
|
labs(title = "Cost-burdened renter share, 2019–2024",
|
||||||
|
subtitle = "Share spending more than 30% of household income on rent",
|
||||||
|
x = NULL, y = NULL,
|
||||||
|
caption = "Source: ACS 5-yr microdata; Civilytics analysis.") +
|
||||||
|
scale_y_continuous(labels = scales::percent_format()) +
|
||||||
|
theme_civilytics()
|
||||||
|
```
|
||||||
|
|
||||||
|
> Across all 12 counties, the steepest increases came in places where
|
||||||
|
> median wages were already in the bottom quartile of the metro region.
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
1. Targeted rental assistance keyed to local wage trajectories.
|
||||||
|
2. Quarterly publication of rent-burden indicators by county.
|
||||||
|
3. Coordinated reporting between HUD and county human-services offices.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: "Quarterly Analysis"
|
||||||
|
subtitle: "A brief look at the numbers."
|
||||||
|
author: "Civilytics Consulting"
|
||||||
|
date: today
|
||||||
|
format: civilytics-reveal-revealjs
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings at a glance
|
||||||
|
|
||||||
|
::: {.columns}
|
||||||
|
::: {.column width="50%"}
|
||||||
|
|
||||||
|
[Key metric]{.stat-number}
|
||||||
|
|
||||||
|
- Point one
|
||||||
|
- Point two
|
||||||
|
- Point three
|
||||||
|
|
||||||
|
:::
|
||||||
|
::: {.column width="50%"}
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
#| fig-width: 6
|
||||||
|
#| fig-height: 4
|
||||||
|
library(ggplot2)
|
||||||
|
library(civilytics)
|
||||||
|
|
||||||
|
ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||||
|
geom_point(size = 2.5) +
|
||||||
|
scale_color_civilytics() +
|
||||||
|
theme_civilytics_slide() +
|
||||||
|
labs(title = "Engine Size vs Fuel Economy",
|
||||||
|
x = "Displacement (L)", y = "Highway MPG")
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Section divider {.section}
|
||||||
|
|
||||||
|
Big idea goes here.
|
||||||
|
|
||||||
|
## Data table
|
||||||
|
|
||||||
|
| County | Burden (%) | Change |
|
||||||
|
|:-------------|:----------:|-------:|
|
||||||
|
| Dane | 38.2 | +4.1 |
|
||||||
|
| Milwaukee | 52.7 | +7.3 |
|
||||||
|
| Hennepin | 41.5 | +3.9 |
|
||||||
|
|
||||||
|
: Cost-burdened renter share, selected counties.
|
||||||
|
|
||||||
|
## A blockquote slide {.pullquote}
|
||||||
|
|
||||||
|
> Rent has outpaced wages in every county we studied.
|
||||||
|
|
||||||
|
Civilytics Consulting, 2026
|
||||||
|
|
||||||
|
## Thank you {.thank-you}
|
||||||
|
|
||||||
|
Questions?
|
||||||
|
|
||||||
|
- jared@civilytics.com
|
||||||
|
- civilytics.com
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
title: Civilytics Reveal
|
||||||
|
author: Civilytics Consulting
|
||||||
|
version: 1.0.0
|
||||||
|
quarto-required: ">=1.4.0"
|
||||||
|
contributes:
|
||||||
|
formats:
|
||||||
|
revealjs:
|
||||||
|
# ---- Theme ----
|
||||||
|
theme: [default, civilytics.scss]
|
||||||
|
|
||||||
|
# ---- Slide geometry ----
|
||||||
|
width: 1280
|
||||||
|
height: 720
|
||||||
|
margin: 0
|
||||||
|
min-scale: 0.2
|
||||||
|
max-scale: 2.0
|
||||||
|
center: false
|
||||||
|
|
||||||
|
# ---- Navigation / chrome ----
|
||||||
|
controls: true
|
||||||
|
controls-layout: edges
|
||||||
|
progress: true
|
||||||
|
slide-number: "c/t"
|
||||||
|
hash-type: number
|
||||||
|
history: true
|
||||||
|
navigation-mode: linear
|
||||||
|
transition: fade
|
||||||
|
transition-speed: fast
|
||||||
|
background-transition: fade
|
||||||
|
|
||||||
|
# ---- Code ----
|
||||||
|
highlight-style: github
|
||||||
|
code-line-numbers: true
|
||||||
|
code-copy: true
|
||||||
|
code-overflow: wrap
|
||||||
|
|
||||||
|
# ---- Math ----
|
||||||
|
html-math-method: mathjax
|
||||||
|
|
||||||
|
# ---- Tables ----
|
||||||
|
tbl-cap-location: top
|
||||||
|
|
||||||
|
# ---- Links / fragments ----
|
||||||
|
link-external-newwindow: true
|
||||||
|
incremental: false
|
||||||
|
|
||||||
|
# ---- Brand chrome (injected by civilytics.js) ----
|
||||||
|
include-in-header:
|
||||||
|
- file: civilytics-head.html
|
||||||
|
include-after-body:
|
||||||
|
- file: civilytics-after.html
|
||||||
|
|
||||||
|
# ---- Footer / logo ----
|
||||||
|
# Override in the document front-matter if desired:
|
||||||
|
# footer: "Civilytics Consulting · 2026"
|
||||||
|
# logo: _extensions/civilytics-reveal/assets/civilytics-mark.svg
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<!-- Civilytics Reveal — brand chrome injector -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
// The Civilytics wordmark is the bold serif "Civilytics" lockup with the pulse
|
||||||
|
// waveform riding inline at the end. It ships as a baked SVG in two variants:
|
||||||
|
// ink-on-paper (default) and paper-on-ink (reverse) for dark backgrounds.
|
||||||
|
// We render it as an <img> so the official artwork is preserved exactly —
|
||||||
|
// the file picks up its color from the SVG itself, not from CSS currentColor.
|
||||||
|
var ASSET_BASE = (function () {
|
||||||
|
// Find the URL this script's <link rel="stylesheet" href="..civilytics.css..">
|
||||||
|
// was loaded from, so we can resolve sibling assets the same way.
|
||||||
|
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
||||||
|
for (var i = 0; i < links.length; i++) {
|
||||||
|
var href = links[i].getAttribute('href') || '';
|
||||||
|
var m = href.match(/^(.*\/)civilytics\.css(\?|$)/);
|
||||||
|
if (m) return m[1] + 'assets/';
|
||||||
|
}
|
||||||
|
// Fallback: assume Quarto-style sibling layout
|
||||||
|
return '_extensions/civilytics-reveal/assets/';
|
||||||
|
})();
|
||||||
|
|
||||||
|
function makeWordmarkImg(reverse) {
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.className = 'civilytics-wordmark';
|
||||||
|
img.alt = 'Civilytics';
|
||||||
|
img.src = ASSET_BASE + (reverse ? 'civilytics-wordmark-reverse.svg' : 'civilytics-wordmark.svg');
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectWordmark(slide, reverse) {
|
||||||
|
if (!slide) return;
|
||||||
|
if (slide.querySelector(':scope > .civilytics-wordmark')) return;
|
||||||
|
slide.insertBefore(makeWordmarkImg(reverse), slide.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (!window.Reveal) { setTimeout(init, 50); return; }
|
||||||
|
var root = document.querySelector('.reveal');
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
// Title slide — paper background, ink wordmark
|
||||||
|
document.querySelectorAll(
|
||||||
|
'.slides #title-slide, .slides section.title-slide'
|
||||||
|
).forEach(function (s) { injectWordmark(s, false); });
|
||||||
|
|
||||||
|
// Thank-you slide — ink background, reversed wordmark
|
||||||
|
document.querySelectorAll('.slides section.thank-you').forEach(function (s) {
|
||||||
|
injectWordmark(s, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Brand footer (pulse glyph + "Civilytics") bottom-left on body slides.
|
||||||
|
// Uses inline SVG so it inherits the slide's text color via currentColor.
|
||||||
|
if (!root.querySelector('.civilytics-footer')) {
|
||||||
|
var footer = document.createElement('div');
|
||||||
|
footer.className = 'civilytics-footer';
|
||||||
|
footer.setAttribute('aria-hidden', 'true');
|
||||||
|
footer.innerHTML =
|
||||||
|
'<svg viewBox="0 0 30 14" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter">' +
|
||||||
|
'<path d="M0 8 L5 8 L8 3 L12 13 L16 1 L20 11 L23 8 L30 8"/>' +
|
||||||
|
'</svg>' +
|
||||||
|
'<span>Civilytics</span>';
|
||||||
|
root.appendChild(footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChrome() {
|
||||||
|
var current = Reveal.getCurrentSlide();
|
||||||
|
if (!current) return;
|
||||||
|
var hide = current.matches('.title-slide, #title-slide, .section-divider, .section, .thank-you, .full-bleed, .no-chrome');
|
||||||
|
document.body.classList.toggle('civilytics-hide-chrome', !!hide);
|
||||||
|
}
|
||||||
|
Reveal.on('ready', updateChrome);
|
||||||
|
Reveal.on('slidechanged', updateChrome);
|
||||||
|
}
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<!-- Civilytics Reveal — fonts preconnect (theme CSS also @imports, this just warms the connection) -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
% Civilytics — title-page partial.
|
||||||
|
% Replaces Quarto's default \maketitle. Uses values from YAML
|
||||||
|
% (\thetitle, \theauthor, \thedate) plus an \ifabstract block.
|
||||||
|
|
||||||
|
% Guard: \thesubtitle is normally defined by civilytics.tex's subtitle
|
||||||
|
% capture; provide a fallback so this partial degrades gracefully if used
|
||||||
|
% without that preamble. See civilyticsR issue #13.
|
||||||
|
\providecommand{\thesubtitle}{}
|
||||||
|
|
||||||
|
\begin{titlepage}
|
||||||
|
\pagecolor{paper}
|
||||||
|
\color{ink}
|
||||||
|
\vspace*{0.5in}
|
||||||
|
{\sffamily\bfseries\scriptsize\color{ember}\MakeUppercase{— Civilytics Consulting}\par}
|
||||||
|
\vspace{12pt}
|
||||||
|
{\displayfont\fontsize{32pt}{34pt}\selectfont\bfseries\color{ink}\thetitle\par}
|
||||||
|
\vspace{8pt}
|
||||||
|
\ifx\thesubtitle\@empty\else
|
||||||
|
{\rmfamily\itshape\fontsize{14pt}{18pt}\selectfont\color{ink2}\thesubtitle\par}
|
||||||
|
\fi
|
||||||
|
\vspace{18pt}
|
||||||
|
\noindent\rule{\linewidth}{2pt}\par
|
||||||
|
\vspace{1pt}
|
||||||
|
\noindent{\color{rule}\rule{\linewidth}{0.4pt}}\par
|
||||||
|
\vspace{8pt}
|
||||||
|
\noindent
|
||||||
|
\begin{minipage}[t]{0.5\linewidth}
|
||||||
|
{\sffamily\scriptsize\color{ink3}\MakeUppercase{Authors}\par}
|
||||||
|
\vspace{2pt}
|
||||||
|
{\sffamily\small\color{ink}\theauthor\par}
|
||||||
|
\end{minipage}\hfill
|
||||||
|
\begin{minipage}[t]{0.4\linewidth}\raggedleft
|
||||||
|
{\sffamily\scriptsize\color{ink3}\MakeUppercase{Published}\par}
|
||||||
|
\vspace{2pt}
|
||||||
|
{\sffamily\small\color{ink}\thedate\par}
|
||||||
|
\end{minipage}\par
|
||||||
|
\vspace{1.5cm}
|
||||||
|
|
||||||
|
% Pulse mark, in ember
|
||||||
|
\begin{center}
|
||||||
|
{\color{ember}\rule{40pt}{2pt}}
|
||||||
|
\end{center}
|
||||||
|
\end{titlepage}
|
||||||
|
\clearpage
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
% =============================================================
|
||||||
|
% Civilytics — LaTeX preamble for Quarto PDF
|
||||||
|
% Usage in YAML:
|
||||||
|
% format:
|
||||||
|
% pdf:
|
||||||
|
% include-in-header: quarto/latex/civilytics.tex
|
||||||
|
% include-before-body: quarto/latex/civilytics-title.tex
|
||||||
|
% Requires: xelatex or lualatex (for system fonts).
|
||||||
|
% =============================================================
|
||||||
|
|
||||||
|
\usepackage{xcolor}
|
||||||
|
\usepackage{fontspec}
|
||||||
|
\usepackage{titlesec}
|
||||||
|
\usepackage{titling}
|
||||||
|
\usepackage{fancyhdr}
|
||||||
|
\usepackage{enumitem}
|
||||||
|
\usepackage{booktabs}
|
||||||
|
\usepackage{caption}
|
||||||
|
\usepackage[hidelinks]{hyperref}
|
||||||
|
\usepackage{microtype}
|
||||||
|
\usepackage{tcolorbox}
|
||||||
|
\tcbuselibrary{skins,breakable}
|
||||||
|
|
||||||
|
% --- Civilytics palette ---
|
||||||
|
\definecolor{paper}{HTML}{FAF7F2}
|
||||||
|
\definecolor{paper2}{HTML}{F2EDE4}
|
||||||
|
\definecolor{ink}{HTML}{0E1A2B}
|
||||||
|
\definecolor{ink2}{HTML}{2B3A52}
|
||||||
|
\definecolor{ink3}{HTML}{5A6A82}
|
||||||
|
\definecolor{rule}{HTML}{D6CEBD}
|
||||||
|
\definecolor{ruleStrong}{HTML}{B8AE97}
|
||||||
|
\definecolor{navy}{HTML}{22406A}
|
||||||
|
\definecolor{ember}{HTML}{C25311}
|
||||||
|
\definecolor{emberDark}{HTML}{923D00}
|
||||||
|
\definecolor{teal}{HTML}{1F6F70}
|
||||||
|
\definecolor{plum}{HTML}{6B3A5E}
|
||||||
|
|
||||||
|
% --- Page color ---
|
||||||
|
\pagecolor{paper}
|
||||||
|
\color{ink}
|
||||||
|
|
||||||
|
% --- Fonts (require local install or fontspec lookup) ---
|
||||||
|
% Bold uses the family's native Bold weight (present in every Source Serif 4
|
||||||
|
% install). Do NOT hard-require a "SemiBold" face: the package installs no
|
||||||
|
% system fonts for the PDF path, and standard Source Serif 4 ships only
|
||||||
|
% Regular/Bold/Italic/BoldItalic. See civilyticsR issue #14.
|
||||||
|
\setmainfont{Source Serif 4}[
|
||||||
|
UprightFont = *,
|
||||||
|
ItalicFont = * Italic,
|
||||||
|
Ligatures = TeX,
|
||||||
|
]
|
||||||
|
\setsansfont{Inter}[Ligatures = TeX]
|
||||||
|
\setmonofont{JetBrains Mono}[Scale = 0.92]
|
||||||
|
\newfontfamily\displayfont{Libre Franklin}[Ligatures = TeX]
|
||||||
|
|
||||||
|
% --- Subtitle capture ---
|
||||||
|
% Quarto/pandoc defines \subtitle (which appends to \@title) but never
|
||||||
|
% \thesubtitle, which the title page uses. This preamble is emitted before
|
||||||
|
% pandoc's \providecommand{\subtitle}, so our definition wins: capture the
|
||||||
|
% subtitle into \thesubtitle instead. See civilyticsR issue #13.
|
||||||
|
\makeatletter
|
||||||
|
\providecommand{\thesubtitle}{}
|
||||||
|
\def\subtitle#1{\renewcommand{\thesubtitle}{#1}}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
% --- Use the Civilytics title page, not pandoc's default ---
|
||||||
|
% civilytics-title.tex (include-before-body) IS the title page. Quarto emits
|
||||||
|
% its default \maketitle + abstract *before* include-before-body, which would
|
||||||
|
% print a second, unstyled title. Neutralise both here, in the preamble
|
||||||
|
% (runs at \begin{document}, before the default title). The branded title page
|
||||||
|
% does not display the abstract. See civilyticsR issue #13.
|
||||||
|
\AtBeginDocument{%
|
||||||
|
\renewcommand{\maketitle}{}%
|
||||||
|
\renewenvironment{abstract}{\setbox0=\vbox\bgroup}{\egroup}%
|
||||||
|
}
|
||||||
|
|
||||||
|
% --- Hyperlinks ---
|
||||||
|
\hypersetup{
|
||||||
|
colorlinks = true,
|
||||||
|
linkcolor = navy,
|
||||||
|
urlcolor = navy,
|
||||||
|
citecolor = navy,
|
||||||
|
filecolor = navy,
|
||||||
|
}
|
||||||
|
|
||||||
|
% --- Section headings ---
|
||||||
|
\titleformat{\section}[block]
|
||||||
|
{\bfseries\Large\displayfont\color{ink}}
|
||||||
|
{}{0pt}
|
||||||
|
{\titlerule[0.4pt]\vspace{4pt}}[\vspace{-6pt}]
|
||||||
|
\titleformat{\subsection}[block]
|
||||||
|
{\bfseries\large\displayfont\color{ink}}{}{0pt}{}
|
||||||
|
\titleformat{\subsubsection}[block]
|
||||||
|
{\bfseries\normalsize\displayfont\color{ink}}{}{0pt}{}
|
||||||
|
|
||||||
|
% --- Page header / footer ---
|
||||||
|
\pagestyle{fancy}
|
||||||
|
\fancyhf{}
|
||||||
|
\renewcommand{\headrulewidth}{0pt}
|
||||||
|
\renewcommand{\footrulewidth}{0pt}
|
||||||
|
\fancyhead[L]{\sffamily\scriptsize\color{ink3}\MakeUppercase{Civilytics Consulting}}
|
||||||
|
\fancyhead[R]{\sffamily\scriptsize\color{ink3}\thetitle}
|
||||||
|
\fancyfoot[L]{\sffamily\scriptsize\color{ink3}civilytics.com}
|
||||||
|
\fancyfoot[C]{\sffamily\scriptsize\color{ink3}\thepage}
|
||||||
|
\fancyfoot[R]{\sffamily\scriptsize\color{ink3}\textcopyright\ 2026}
|
||||||
|
|
||||||
|
% --- Captions ---
|
||||||
|
\captionsetup{
|
||||||
|
font={sf,small,color=ink3},
|
||||||
|
labelfont={sf,bf,color=ink3},
|
||||||
|
labelsep=period,
|
||||||
|
justification=raggedright,
|
||||||
|
singlelinecheck=false,
|
||||||
|
}
|
||||||
|
|
||||||
|
% --- Pullquote ---
|
||||||
|
\newtcolorbox{pullquote}{
|
||||||
|
enhanced, breakable, frame hidden, colback=paper,
|
||||||
|
borderline west = {2pt}{0pt}{ember},
|
||||||
|
left=14pt, right=4pt, top=4pt, bottom=4pt,
|
||||||
|
fontupper={\itshape\rmfamily\large\color{ink}},
|
||||||
|
}
|
||||||
|
|
||||||
|
% --- Code blocks (Pandoc + listings or minted both inherit colors below) ---
|
||||||
|
\definecolor{codebg}{HTML}{0E1A2B}
|
||||||
|
\definecolor{codefg}{HTML}{FAF7F2}
|
||||||
|
|
||||||
|
% --- Lists tighter ---
|
||||||
|
\setlist{itemsep=2pt, topsep=4pt}
|
||||||
|
|
||||||
|
% --- Tabular numerals ---
|
||||||
|
\newcommand{\tnum}[1]{{\addfontfeature{Numbers={Tabular,Lining}}#1}}
|
||||||
|
|
||||||
|
% --- Ember accent rule above section starts ---
|
||||||
|
\newcommand{\emberrule}{{\color{ember}\rule{60pt}{3pt}}\par\vspace{4pt}}
|
||||||
@@ -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;
|
||||||
@@ -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; }
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Civilytics — Typst show/entry partial for Quarto (typst-show.typ).
|
||||||
|
// Pairs with typst-template.typ. Quarto appends the rendered document body
|
||||||
|
// after this partial, so this file intentionally ends with the show rule and
|
||||||
|
// no trailing body token. (Do not write that token in a comment here: Quarto
|
||||||
|
// interpolates its template variables even inside comments.)
|
||||||
|
// Title/subtitle/date are wrapped in [ ] so arbitrary text (including words
|
||||||
|
// that are Typst keywords like "for"/"in") is treated as content, not code.
|
||||||
|
// See civilyticsR issue #12.
|
||||||
|
#show: doc => civilytics(
|
||||||
|
title: [$title$],
|
||||||
|
$if(subtitle)$subtitle: [$subtitle$],$endif$
|
||||||
|
$if(by-author)$authors: ($for(by-author)$"$it.name.literal$",$endfor$),$endif$
|
||||||
|
$if(date)$date: [$date$],$endif$
|
||||||
|
$if(abstract)$abstract: [$abstract$],$endif$
|
||||||
|
toc: $if(toc)$true$else$false$endif$,
|
||||||
|
doc
|
||||||
|
)
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
// =============================================================
|
||||||
|
// Civilytics — Typst template partial for Quarto PDF (typst-template.typ).
|
||||||
|
// Shipped as a Quarto template-partial (paired with typst-show.typ) rather
|
||||||
|
// than a full `template:` so Quarto keeps its own `definitions` partial —
|
||||||
|
// which defines Skylighting/token functions needed for syntax-highlighted
|
||||||
|
// code blocks. See civilyticsR issue #12.
|
||||||
|
// Usage in YAML:
|
||||||
|
// format:
|
||||||
|
// typst:
|
||||||
|
// template-partials:
|
||||||
|
// - quarto/typst/typst-template.typ
|
||||||
|
// - quarto/typst/typst-show.typ
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
#let paper-bg = rgb("#FAF7F2")
|
||||||
|
#let ink = rgb("#0E1A2B")
|
||||||
|
#let ink-2 = rgb("#2B3A52")
|
||||||
|
#let ink-3 = rgb("#5A6A82")
|
||||||
|
#let rule = rgb("#D6CEBD")
|
||||||
|
#let rule-strong = rgb("#B8AE97")
|
||||||
|
#let navy = rgb("#22406A")
|
||||||
|
#let ember = rgb("#C25311")
|
||||||
|
#let ember-dark = rgb("#923D00")
|
||||||
|
#let plum = rgb("#6B3A5E")
|
||||||
|
#let teal = rgb("#1F6F70")
|
||||||
|
|
||||||
|
#let serif-stack = ("Source Serif 4", "Source Serif Pro", "Georgia", "Times New Roman")
|
||||||
|
#let sans-stack = ("Inter", "Helvetica Neue", "Arial")
|
||||||
|
#let display-stack = ("Libre Franklin", "Inter", "Helvetica Neue")
|
||||||
|
#let mono-stack = ("JetBrains Mono", "Menlo", "Consolas")
|
||||||
|
|
||||||
|
#let civilytics(
|
||||||
|
title: none,
|
||||||
|
subtitle: none,
|
||||||
|
authors: (),
|
||||||
|
date: none,
|
||||||
|
abstract: none,
|
||||||
|
toc: true,
|
||||||
|
doc,
|
||||||
|
) = {
|
||||||
|
// Page setup
|
||||||
|
set page(
|
||||||
|
paper: "us-letter",
|
||||||
|
margin: (top: 1in, bottom: 1in, left: 1.1in, right: 1.1in),
|
||||||
|
fill: paper-bg,
|
||||||
|
header: context {
|
||||||
|
if counter(page).get().first() > 1 {
|
||||||
|
set text(font: sans-stack, size: 8pt, fill: ink-3, tracking: 0.06em)
|
||||||
|
upper[Civilytics Consulting]
|
||||||
|
h(1fr)
|
||||||
|
upper(if title != none { title } else { "" })
|
||||||
|
v(2pt)
|
||||||
|
line(length: 100%, stroke: 0.4pt + rule)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
footer: context {
|
||||||
|
set text(font: sans-stack, size: 8pt, fill: ink-3)
|
||||||
|
line(length: 100%, stroke: 0.4pt + rule)
|
||||||
|
v(4pt)
|
||||||
|
grid(
|
||||||
|
columns: (1fr, auto, 1fr),
|
||||||
|
align: (left, center, right),
|
||||||
|
[civilytics.com],
|
||||||
|
counter(page).display("1 / 1", both: true),
|
||||||
|
[© 2026]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Body text
|
||||||
|
set text(font: serif-stack, size: 10.5pt, fill: ink, lang: "en")
|
||||||
|
set par(justify: false, leading: 0.6em, first-line-indent: 0pt)
|
||||||
|
|
||||||
|
// Headings
|
||||||
|
show heading.where(level: 1): it => {
|
||||||
|
v(1.4em)
|
||||||
|
block[
|
||||||
|
#set text(font: display-stack, weight: 900, size: 22pt, fill: ink, tracking: -0.025em)
|
||||||
|
#it.body
|
||||||
|
]
|
||||||
|
v(0.2em)
|
||||||
|
line(length: 60pt, stroke: 3pt + ember)
|
||||||
|
v(0.6em)
|
||||||
|
}
|
||||||
|
show heading.where(level: 2): it => {
|
||||||
|
v(1.1em)
|
||||||
|
line(length: 100%, stroke: 0.5pt + rule)
|
||||||
|
v(0.5em)
|
||||||
|
block[
|
||||||
|
#set text(font: display-stack, weight: 800, size: 16pt, fill: ink, tracking: -0.02em)
|
||||||
|
#it.body
|
||||||
|
]
|
||||||
|
v(0.3em)
|
||||||
|
}
|
||||||
|
show heading.where(level: 3): it => {
|
||||||
|
v(0.8em)
|
||||||
|
block[
|
||||||
|
#set text(font: display-stack, weight: 700, size: 13pt, fill: ink, tracking: -0.015em)
|
||||||
|
#it.body
|
||||||
|
]
|
||||||
|
v(0.2em)
|
||||||
|
}
|
||||||
|
show heading.where(level: 4): it => block[
|
||||||
|
#set text(font: sans-stack, weight: 600, size: 11pt, fill: ink-2)
|
||||||
|
#it.body
|
||||||
|
]
|
||||||
|
|
||||||
|
// Links
|
||||||
|
show link: it => text(fill: navy, underline(it))
|
||||||
|
|
||||||
|
// Inline code
|
||||||
|
show raw.where(block: false): it => box(
|
||||||
|
fill: rgb("#F2EDE4"),
|
||||||
|
inset: (x: 3pt, y: 1pt),
|
||||||
|
outset: (y: 2pt),
|
||||||
|
radius: 1pt,
|
||||||
|
text(font: mono-stack, size: 0.92em, fill: ember-dark)[#it]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Code blocks
|
||||||
|
show raw.where(block: true): it => block(
|
||||||
|
fill: ink,
|
||||||
|
width: 100%,
|
||||||
|
inset: 12pt,
|
||||||
|
radius: 4pt,
|
||||||
|
)[
|
||||||
|
#set text(font: mono-stack, size: 8.5pt, fill: paper-bg)
|
||||||
|
#it
|
||||||
|
]
|
||||||
|
|
||||||
|
// Block quote
|
||||||
|
show quote.where(block: true): it => block(
|
||||||
|
stroke: (left: 2pt + ember),
|
||||||
|
inset: (left: 14pt, top: 4pt, bottom: 4pt),
|
||||||
|
spacing: 1.2em,
|
||||||
|
)[
|
||||||
|
#set text(font: serif-stack, size: 13pt, style: "italic", fill: ink)
|
||||||
|
#it.body
|
||||||
|
]
|
||||||
|
|
||||||
|
// Figures
|
||||||
|
show figure: it => block(spacing: 1.4em)[
|
||||||
|
#it.body
|
||||||
|
#v(4pt)
|
||||||
|
#line(length: 100%, stroke: 0.4pt + rule)
|
||||||
|
#v(2pt)
|
||||||
|
#set text(font: sans-stack, size: 8.5pt, fill: ink-3)
|
||||||
|
#it.caption
|
||||||
|
]
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
set table(
|
||||||
|
stroke: (x, y) => (
|
||||||
|
top: if y == 0 { 1.5pt + ink } else if y == 1 { 0.6pt + rule-strong } else { 0pt },
|
||||||
|
bottom: if y == 0 { 0pt } else { 0.4pt + rule },
|
||||||
|
),
|
||||||
|
inset: (x: 8pt, y: 6pt),
|
||||||
|
)
|
||||||
|
show table.cell.where(y: 0): set text(
|
||||||
|
font: sans-stack, size: 8pt, weight: 600, fill: ink-3, tracking: 0.06em
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----- Title block -----
|
||||||
|
if title != none {
|
||||||
|
block[
|
||||||
|
#set text(font: sans-stack, size: 8pt, weight: 600, fill: ember, tracking: 0.1em)
|
||||||
|
#upper[— Civilytics Consulting]
|
||||||
|
]
|
||||||
|
v(8pt)
|
||||||
|
block[
|
||||||
|
#set text(font: display-stack, weight: 900, size: 32pt, fill: ink, tracking: -0.035em)
|
||||||
|
#set par(leading: 0.4em)
|
||||||
|
#title
|
||||||
|
]
|
||||||
|
if subtitle != none {
|
||||||
|
v(8pt)
|
||||||
|
block[
|
||||||
|
#set text(font: serif-stack, style: "italic", size: 14pt, fill: ink-2)
|
||||||
|
#set par(leading: 0.55em)
|
||||||
|
#subtitle
|
||||||
|
]
|
||||||
|
}
|
||||||
|
v(18pt)
|
||||||
|
line(length: 100%, stroke: 3pt + ink)
|
||||||
|
v(2pt)
|
||||||
|
line(length: 100%, stroke: 0.4pt + rule)
|
||||||
|
v(8pt)
|
||||||
|
|
||||||
|
// Authors + date row
|
||||||
|
grid(
|
||||||
|
columns: (1fr, 1fr),
|
||||||
|
align: (left, right),
|
||||||
|
[
|
||||||
|
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
|
||||||
|
#upper[Authors] \
|
||||||
|
#set text(font: sans-stack, size: 10pt, fill: ink, weight: 500, tracking: 0em)
|
||||||
|
#if authors.len() > 0 {
|
||||||
|
authors.map(a => if type(a) == str { a } else { a.name }).join(", ")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
|
||||||
|
#upper[Published] \
|
||||||
|
#set text(font: sans-stack, size: 10pt, fill: ink, weight: 500, tracking: 0em)
|
||||||
|
#if date != none { date }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
v(24pt)
|
||||||
|
|
||||||
|
if abstract != none {
|
||||||
|
block(
|
||||||
|
stroke: (left: 2pt + ember),
|
||||||
|
inset: (left: 14pt),
|
||||||
|
)[
|
||||||
|
#set text(font: serif-stack, style: "italic", size: 12pt, fill: ink)
|
||||||
|
#set par(leading: 0.55em)
|
||||||
|
#abstract
|
||||||
|
]
|
||||||
|
v(20pt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if toc {
|
||||||
|
block[
|
||||||
|
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
|
||||||
|
#upper[Contents]
|
||||||
|
]
|
||||||
|
v(4pt)
|
||||||
|
line(length: 100%, stroke: 0.4pt + rule)
|
||||||
|
v(8pt)
|
||||||
|
outline(title: none, indent: auto, depth: 3)
|
||||||
|
v(20pt)
|
||||||
|
pagebreak()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doc
|
||||||
|
}
|
||||||
@@ -4,7 +4,13 @@
|
|||||||
\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,
|
||||||
|
logo,
|
||||||
|
margin_param = NULL,
|
||||||
|
font_scale = 1.1,
|
||||||
|
position = c("bottom-right", "bottom-left", "top-right", "top-left")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
\arguments{
|
\arguments{
|
||||||
\item{plot}{a ggplot2 grob}
|
\item{plot}{a ggplot2 grob}
|
||||||
@@ -12,6 +18,18 @@ add_logo(plot, logo, margin_param = NULL)
|
|||||||
\item{logo}{a logo grob created by make_logo_grob()}
|
\item{logo}{a logo grob created by make_logo_grob()}
|
||||||
|
|
||||||
\item{margin_param}{a numeric specifying what margin to add or subtract to align the logo}
|
\item{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{
|
\description{
|
||||||
Add a logo to a ggplot2 object
|
Add a logo to a ggplot2 object
|
||||||
|
|||||||
@@ -2,9 +2,16 @@
|
|||||||
% 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,
|
||||||
|
logo,
|
||||||
|
nrow = 1,
|
||||||
|
widths = NULL,
|
||||||
|
margin_param = NULL,
|
||||||
|
font_scale = 1.1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
\arguments{
|
\arguments{
|
||||||
\item{plot_list}{a list containing ggplot2 objects}
|
\item{plot_list}{a list containing ggplot2 objects}
|
||||||
@@ -16,14 +23,21 @@ add_logo_ga(plot_list, logo, nrow = 1, widths = NULL, margin_param = NULL)
|
|||||||
\item{widths}{an optional vector the same length as plot_list with the widths for each plot}
|
\item{widths}{an optional vector the same length as plot_list with the widths for each plot}
|
||||||
|
|
||||||
\item{margin_param}{a number giving the adjustment up or down to help manually align logo and captions}
|
\item{margin_param}{a number giving the adjustment up or down to help manually align logo and captions}
|
||||||
|
|
||||||
|
\item{font_scale}{Numeric. Multiplicative scaling factor applied to text
|
||||||
|
sizes before composing. Default `1.1`. See [add_logo()] for details.}
|
||||||
|
}
|
||||||
|
\value{
|
||||||
|
a grid object
|
||||||
}
|
}
|
||||||
\description{
|
\description{
|
||||||
Add a logo to a ggplot2 object
|
Add a logo to multiple ggplot2 objects
|
||||||
}
|
}
|
||||||
\note{
|
\note{
|
||||||
The resulting object needs to be drawn to the screen using grid.draw()
|
The resulting object needs to be drawn to the screen using grid.draw()
|
||||||
}
|
}
|
||||||
\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()
|
||||||
@@ -31,3 +45,4 @@ plot_and_logo <- add_logo(tmp_plot, tmp_logo)
|
|||||||
grid.draw(plot_and_logo)
|
grid.draw(plot_and_logo)
|
||||||
dev.off()
|
dev.off()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
@@ -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}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
% 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(force = FALSE)
|
||||||
|
}
|
||||||
|
\arguments{
|
||||||
|
\item{force}{Logical. If \code{TRUE}, reload fonts even if they were already
|
||||||
|
loaded in this session. Default \code{FALSE}.}
|
||||||
|
}
|
||||||
|
\value{
|
||||||
|
Invisibly returns \code{TRUE} if fonts were loaded, \code{FALSE} if skipped
|
||||||
|
(already loaded and \code{force = FALSE}).
|
||||||
|
}
|
||||||
|
\description{
|
||||||
|
Downloads Inter, Libre Franklin, Source Serif 4 and JetBrains Mono from
|
||||||
|
Google Fonts via \code{sysfonts::font_add_google()}, then calls
|
||||||
|
\code{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).
|
||||||
|
|
||||||
|
Subsequent calls within the same session are no-ops unless \code{force = TRUE}.
|
||||||
|
}
|
||||||
|
\examples{
|
||||||
|
\dontrun{
|
||||||
|
civilytics_load_fonts()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ countDots(x)
|
|||||||
\arguments{
|
\arguments{
|
||||||
\item{x}{a character vector}
|
\item{x}{a character vector}
|
||||||
}
|
}
|
||||||
|
\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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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}
|
||||||
@@ -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}
|
||||||
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ 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{
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,12 @@ 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{
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||