Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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\.user$
|
||||
^\.github$
|
||||
^\.gitea$
|
||||
^README\.Rmd$
|
||||
^Makefile$
|
||||
^Jenkinsfile$
|
||||
^Dockerfile$
|
||||
^LICENSE\.md$
|
||||
^\.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}
|
||||
@@ -1,24 +1,37 @@
|
||||
Package: civilytics
|
||||
Type: Package
|
||||
Title: Utilities Functions for Civilytics
|
||||
Version: 0.1.0
|
||||
Author: Jared E. Knowles <jared@civilytics.com>
|
||||
Maintainer: Jared E. Knowles <jared@civilytics.com>
|
||||
Description: House R functions for Civilytics Consulting LLC
|
||||
This package implements a variety of useful functions for creating and
|
||||
branding analyses produced by Civilytics Consulting LLC.
|
||||
License: LICENSE
|
||||
Title: Brand Themes, Color Palettes, and Utility Functions for Civilytics
|
||||
Version: 0.2.0
|
||||
Authors@R:
|
||||
person("Jared", "E. Knowles", email = "jared@civilytics.com",
|
||||
role = c("aut", "cre"))
|
||||
Description: Provides a complete ggplot2 brand theme system for Civilytics
|
||||
Consulting LLC, including editorial light, dark, and slide-optimized themes;
|
||||
10 curated color palettes for qualitative, sequential, and diverging data;
|
||||
logo composition utilities; Quarto themes for HTML reports, PDF, Typst, and
|
||||
Reveal.js presentations; and data-wrangling helpers for public-sector
|
||||
analysis.
|
||||
License: LGPL (>= 3)
|
||||
URL: https://gitea.civilytics.org/Civilytics/civilyticsR
|
||||
BugReports: https://gitea.civilytics.org/Civilytics/civilyticsR/issues
|
||||
Depends:
|
||||
R (>= 2.15.1)
|
||||
R (>= 4.1.0)
|
||||
Imports:
|
||||
ggplot2,
|
||||
ggplot2 (>= 4.0.0),
|
||||
jpeg,
|
||||
jsonlite,
|
||||
png,
|
||||
stringr,
|
||||
gridExtra,
|
||||
grid
|
||||
grid,
|
||||
stringdist,
|
||||
showtext,
|
||||
sysfonts
|
||||
Encoding: UTF-8
|
||||
LazyData: true
|
||||
Suggests:
|
||||
testthat
|
||||
RoxygenNote: 7.2.1
|
||||
testthat (>= 3.0.0),
|
||||
tidycensus,
|
||||
quarto
|
||||
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 \
|
||||
libssl-dev \
|
||||
libssh2-1-dev \
|
||||
libudunits2-dev \
|
||||
libgdal-dev \
|
||||
libgeos-dev \
|
||||
libproj-dev \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /var/lib/shiny-server/bookmarks/shiny
|
||||
|
||||
RUN install2.r ggplot2 jpeg png stringr gridExtra grid testthat
|
||||
|
||||
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,75 @@
|
||||
# Generated by roxygen2: do not edit by hand
|
||||
|
||||
export(beep)
|
||||
export(add_logo)
|
||||
export(add_logo_ga)
|
||||
export(agresti_coull_interval)
|
||||
export(civilytics_colors)
|
||||
export(civilytics_load_fonts)
|
||||
export(civilytics_logo)
|
||||
export(civilytics_pal)
|
||||
export(civilytics_palette)
|
||||
export(civilytics_palettes)
|
||||
export(clopper_pearson)
|
||||
export(countCleanr)
|
||||
export(countDots)
|
||||
export(countNA)
|
||||
export(dbSafeNames)
|
||||
export(findDots)
|
||||
export(get_fips)
|
||||
export(get_png)
|
||||
export(get_stabbr)
|
||||
export(grade_level_to_num)
|
||||
export(has_caption)
|
||||
export(make_logo_grob)
|
||||
export(match_test)
|
||||
export(measure_caption)
|
||||
export(na_sum)
|
||||
export(na_zero)
|
||||
export(nvals)
|
||||
export(outersect)
|
||||
export(perturb_count)
|
||||
export(plot_jpeg)
|
||||
export(postcode_lookup)
|
||||
export(pretty_count)
|
||||
export(pretty_per)
|
||||
export(race_short_names)
|
||||
export(random_round)
|
||||
export(rnh)
|
||||
export(round_to_nearest_half)
|
||||
export(safe_max)
|
||||
export(safe_ratio)
|
||||
export(scale_color_civilytics)
|
||||
export(scale_fill_civilytics)
|
||||
export(simpleCap)
|
||||
export(star_subs)
|
||||
export(theme_civilytics)
|
||||
export(theme_civilytics_dark)
|
||||
export(theme_civilytics_dark_map)
|
||||
export(theme_civilytics_map)
|
||||
export(theme_civilytics_slide)
|
||||
export(theme_civilytics_slide_map)
|
||||
export(trim_max)
|
||||
export(use_civilytics_brand)
|
||||
export(use_civilytics_revealjs)
|
||||
export(use_civilytics_theme)
|
||||
export(waldInterval)
|
||||
export(z_gap_test)
|
||||
export(z_univariate)
|
||||
import(ggplot2)
|
||||
importFrom(ggplot2,annotation_custom)
|
||||
importFrom(ggplot2,ggplot)
|
||||
importFrom(ggplot2,theme)
|
||||
importFrom(graphics,plot)
|
||||
importFrom(ggplot2,theme_void)
|
||||
importFrom(graphics,rasterImage)
|
||||
importFrom(grid,grid.draw)
|
||||
importFrom(grid,rasterGrob)
|
||||
importFrom(gridExtra,arrangeGrob)
|
||||
importFrom(jpeg,readJPEG)
|
||||
importFrom(png,readPNG)
|
||||
importFrom(stats,qbeta)
|
||||
importFrom(stats,qnorm)
|
||||
importFrom(stats,runif)
|
||||
importFrom(stringdist,stringsim)
|
||||
importFrom(stringr,str_count)
|
||||
importFrom(utils,flush.console)
|
||||
|
||||
@@ -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
|
||||
#' [ggplot2::discrete_scale()] or similar scale constructors.
|
||||
#'
|
||||
#' @param name Character. Palette name. See `names(civilytics_palettes)`.
|
||||
#' @param reverse Logical. Reverse the palette order. Default `FALSE`.
|
||||
#'
|
||||
#' @return A function that takes integer `n` and returns `n` hex color codes.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' pal_fn <- civilytics_pal("qual")
|
||||
#' pal_fn(4)
|
||||
civilytics_pal <- function(name = "qual", reverse = FALSE) {
|
||||
function(n) civilytics_palette(name, n = n, reverse = reverse)
|
||||
}
|
||||
|
||||
|
||||
#' Civilytics color scale for ggplot2
|
||||
#'
|
||||
#' Applies a Civilytics brand palette to the `colour` aesthetic.
|
||||
#'
|
||||
#' @param palette Character. Palette name. Defaults to `"qual"`.
|
||||
#' @param discrete Logical. `TRUE` (default) for categorical data; `FALSE`
|
||||
#' for a continuous gradient via [ggplot2::scale_color_gradientn()].
|
||||
#' @param reverse Logical. Reverse the palette order. Default `FALSE`.
|
||||
#' @param ... Additional arguments passed to the ggplot2 scale function.
|
||||
#'
|
||||
#' @return A ggplot2 scale object.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' library(ggplot2)
|
||||
#' ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||
#' geom_point() +
|
||||
#' scale_color_civilytics()
|
||||
#'
|
||||
#' ggplot(mpg, aes(displ, hwy, colour = cty)) +
|
||||
#' geom_point() +
|
||||
#' scale_color_civilytics("seq_navy", discrete = FALSE)
|
||||
scale_color_civilytics <- function(palette = "qual", discrete = TRUE,
|
||||
reverse = FALSE, ...) {
|
||||
if (discrete) {
|
||||
ggplot2::discrete_scale(
|
||||
"colour",
|
||||
palette = civilytics_pal(palette, reverse = reverse),
|
||||
...
|
||||
)
|
||||
} else {
|
||||
pal <- civilytics_palette(palette, reverse = reverse)
|
||||
ggplot2::scale_color_gradientn(
|
||||
colours = grDevices::colorRampPalette(pal)(256),
|
||||
...
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' Civilytics fill scale for ggplot2
|
||||
#'
|
||||
#' Applies a Civilytics brand palette to the `fill` aesthetic.
|
||||
#'
|
||||
#' @param palette Character. Palette name. Defaults to `"qual"`.
|
||||
#' @param discrete Logical. `TRUE` (default) for categorical data; `FALSE`
|
||||
#' for a continuous gradient via [ggplot2::scale_fill_gradientn()].
|
||||
#' @param reverse Logical. Reverse the palette order. Default `FALSE`.
|
||||
#' @param ... Additional arguments passed to the ggplot2 scale function.
|
||||
#'
|
||||
#' @return A ggplot2 scale object.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' library(ggplot2)
|
||||
#' ggplot(mpg, aes(class, fill = class)) +
|
||||
#' geom_bar() +
|
||||
#' scale_fill_civilytics()
|
||||
#'
|
||||
#' ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
|
||||
#' geom_tile() +
|
||||
#' scale_fill_civilytics("seq_ember", discrete = FALSE)
|
||||
scale_fill_civilytics <- function(palette = "qual", discrete = TRUE,
|
||||
reverse = FALSE, ...) {
|
||||
if (discrete) {
|
||||
ggplot2::discrete_scale(
|
||||
"fill",
|
||||
palette = civilytics_pal(palette, reverse = reverse),
|
||||
...
|
||||
)
|
||||
} else {
|
||||
pal <- civilytics_palette(palette, reverse = reverse)
|
||||
ggplot2::scale_fill_gradientn(
|
||||
colours = grDevices::colorRampPalette(pal)(256),
|
||||
...
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,6 @@ countCleanr <- function(x){
|
||||
#'
|
||||
#' @return the names of columns in the dataframe with one or more entries equal to "."
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
findDots <- function(data){
|
||||
return(names(data)[lapply(data, countDots) > 0])
|
||||
|
||||
@@ -49,9 +47,8 @@ findDots <- function(data){
|
||||
#' @param x a character vector
|
||||
#'
|
||||
#' @return
|
||||
#' An integer counting the number of "." occurences in a vector
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
countDots <- function(x){
|
||||
len <- length(x[x == "." & !is.na(x)])
|
||||
totlen <- length(x)
|
||||
@@ -65,8 +62,6 @@ countDots <- function(x){
|
||||
#'
|
||||
#' @return a vector the same length as the input vector with clean names
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
dbSafeNames <- function(names) {
|
||||
names = gsub('[^a-z0-9]+','_',tolower(names))
|
||||
names = make.names(names, unique = TRUE, allow_ = TRUE)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Font family name constants used as defaults in theme functions.
|
||||
# These match the --cv-font-* CSS custom properties on www.civilytics.com.
|
||||
CV_FONT_DISPLAY <- "Libre Franklin" # headings / display text
|
||||
CV_FONT_SANS <- "Inter" # axis text, legends, UI elements
|
||||
CV_FONT_SERIF <- "Source Serif 4" # body prose / editorial long-form
|
||||
CV_FONT_MONO <- "JetBrains Mono" # code, data tables, numeric callouts
|
||||
|
||||
# Internal flag so civilytics_load_fonts() is idempotent within a session.
|
||||
.cv_fonts_loaded <- FALSE
|
||||
|
||||
#' Load Civilytics brand fonts
|
||||
#'
|
||||
#' Downloads Inter and Libre Franklin from Google Fonts via
|
||||
#' [sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so
|
||||
#' that all graphics devices render text with those fonts. This is called
|
||||
#' automatically when the package loads; use this function to retry if the
|
||||
#' initial load failed (e.g., the machine was offline at load time).
|
||||
#'
|
||||
#' @return Invisibly returns `NULL`.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' civilytics_load_fonts()
|
||||
#' }
|
||||
civilytics_load_fonts <- function() {
|
||||
sysfonts::font_add_google("Inter", family = "Inter")
|
||||
sysfonts::font_add_google("Libre Franklin", family = "Libre Franklin")
|
||||
sysfonts::font_add_google("Source Serif 4", family = "Source Serif 4")
|
||||
sysfonts::font_add_google("JetBrains Mono", family = "JetBrains Mono")
|
||||
showtext::showtext_auto()
|
||||
.cv_fonts_loaded <<- TRUE
|
||||
invisible(NULL)
|
||||
}
|
||||
|
||||
.onLoad <- function(libname, pkgname) {
|
||||
tryCatch(
|
||||
civilytics_load_fonts(),
|
||||
error = function(e) {
|
||||
# Store the failure so .onAttach can surface a message to the user.
|
||||
options(.civilytics_fonts_failed = TRUE)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
.onAttach <- function(libname, pkgname) {
|
||||
if (isTRUE(getOption(".civilytics_fonts_failed"))) {
|
||||
packageStartupMessage(
|
||||
"[civilytics] Brand fonts (Inter, Libre Franklin, Source Serif 4, ",
|
||||
"JetBrains Mono) could not be loaded from Google Fonts. ",
|
||||
"Charts will fall back to system fonts. ",
|
||||
"Call civilytics_load_fonts() once you have an internet connection."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Join utilities
|
||||
#' Test the join between two sets of identifiers
|
||||
#'
|
||||
#' @param x a vector of identifiers to check against y
|
||||
#' @param y a vector of identifiers to check against x
|
||||
#' @param distinct logical, should duplicate values of x and y be removed before testing
|
||||
#'
|
||||
#' @return nothing, print a summary of match statistics to the console
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' x <- LETTERS
|
||||
#' y <- c(letters, LETTERS)
|
||||
#' match_test(x, y)
|
||||
match_test <- function(x, y, distinct = TRUE) {
|
||||
if (distinct) {
|
||||
x <- unique(x)
|
||||
y <- unique(y)
|
||||
cat("**** Distinct Matches ****")
|
||||
cat("\n")
|
||||
|
||||
}
|
||||
|
||||
# TODO: DO not report 100% if there is even 1 mismatch
|
||||
|
||||
xiny <- sum(x %in% y)
|
||||
total_x <- length(x)
|
||||
|
||||
yinx <- sum(y %in% x)
|
||||
total_y <- length(y)
|
||||
|
||||
cat("**** Match Summary ****")
|
||||
cat("\n")
|
||||
cat("X in Y")
|
||||
cat("\n")
|
||||
cat(paste0("Of the ", total_x, " X values, ", xiny, " (",
|
||||
100*round(xiny/total_x, 2), "%) were matched."))
|
||||
cat("\n")
|
||||
cat("********************************************")
|
||||
cat("\n")
|
||||
cat("Y in X")
|
||||
cat("\n")
|
||||
cat(paste0("Of the ", total_y, " Y values, ", yinx, " (",
|
||||
100*round(yinx/total_y, 2), "%) were matched."))
|
||||
cat("\n")
|
||||
cat("******************************************")
|
||||
|
||||
}
|
||||
@@ -9,11 +9,13 @@
|
||||
#' @importFrom jpeg readJPEG
|
||||
#' @importFrom graphics rasterImage
|
||||
#' @examples
|
||||
#' img <- system.file("img","Civilytics Consulting Logo.jpg",package="civilytics")
|
||||
#' \dontrun{
|
||||
#' img <- system.file("img","Knowles_Headshot_2019_good.jpg",package="civilytics")
|
||||
#' plot_jpeg(img)
|
||||
#' }
|
||||
plot_jpeg <- function(path, add=FALSE, upscale = TRUE)
|
||||
{
|
||||
jpg = readJPEG(path, native = T) # read the file
|
||||
jpg = readJPEG(path, native = TRUE) # read the file
|
||||
res = dim(jpg)[2:1] # get the resolution, [x, y]
|
||||
if (upscale){
|
||||
res <- res * 3
|
||||
@@ -30,13 +32,10 @@ plot_jpeg <- function(path, add=FALSE, upscale = TRUE)
|
||||
#'
|
||||
#' @param filename a character with file path to a png file
|
||||
#'
|
||||
#' @return
|
||||
#' @return a plotted rasteGrob of a png image
|
||||
#' @export
|
||||
#' @importFrom png readPNG
|
||||
#' @importFrom grid rasterGrob
|
||||
#'
|
||||
#' @examples
|
||||
#'
|
||||
get_png <- function(filename) {
|
||||
grid::rasterGrob(png::readPNG(filename), interpolate = TRUE)
|
||||
}
|
||||
@@ -47,19 +46,51 @@ get_png <- function(filename) {
|
||||
#' @param plot a ggplot2 grob
|
||||
#' @param logo a logo grob created by make_logo_grob()
|
||||
#' @param margin_param a numeric specifying what margin to add or subtract to align the logo
|
||||
#' @param font_scale Numeric. Multiplicative scaling factor applied to text
|
||||
#' sizes before composing the plot with the logo. Default `1.1` inflates
|
||||
#' text by ~10 \% to compensate for the viewport shrinkage caused by
|
||||
#' [gridExtra::arrangeGrob()]. Set to `1` to disable.
|
||||
#' @param position Character. Corner placement for the logo: `"bottom-right"`
|
||||
#' (default), `"bottom-left"`, `"top-right"`, or `"top-left"`. Controls
|
||||
#' whether the logo is placed above or below the plot.
|
||||
#'
|
||||
#' @return
|
||||
#' @return a grob with a logo attached to it ready to plot
|
||||
#' @importFrom ggplot2 theme
|
||||
#' @importFrom gridExtra arrangeGrob
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
add_logo <- function(plot, logo, margin_param = NULL) {
|
||||
add_logo <- function(plot, logo, margin_param = NULL, font_scale = 1.1,
|
||||
position = c("bottom-right", "bottom-left",
|
||||
"top-right", "top-left")) {
|
||||
position <- match.arg(position)
|
||||
at_top <- grepl("top", position)
|
||||
|
||||
# Capture the plot's background color so we can fill the entire composed
|
||||
# grob with it. The logo grob must stay transparent (theme_void) so that
|
||||
# caption/axis text overlapping via negative margins remains visible.
|
||||
bg_fill <- plot$theme$plot.background$fill
|
||||
|
||||
# Inflate text sizes to compensate for arrangeGrob viewport shrinkage.
|
||||
# All theme text elements use rel() sizing, so scaling the root 'text'
|
||||
# element cascades to titles, axis labels, legends, captions, and strips.
|
||||
if (!is.null(font_scale) && font_scale != 1) {
|
||||
base_size <- plot$theme$text$size %||% 14
|
||||
plot <- plot + ggplot2::theme(
|
||||
text = ggplot2::element_text(size = base_size * font_scale)
|
||||
)
|
||||
}
|
||||
|
||||
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)) {
|
||||
# convert the caption size to a negative number and on the "pt" scale
|
||||
# p1$theme$plot.caption$size * 1.1
|
||||
# Count the number of lines, which is this + 1
|
||||
# For each line, we can add a certain negative space to align the logo
|
||||
cap_lines <- measure_caption(plot)
|
||||
if (!is.null(margin_param)) {
|
||||
plot <- plot + theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt"))
|
||||
@@ -69,9 +100,19 @@ add_logo <- function(plot, logo, margin_param = NULL) {
|
||||
} else {
|
||||
plot <- plot + theme(plot.margin = unit(c(7, 7, -14, 7), "pt"))
|
||||
}
|
||||
|
||||
arrangeGrob(plot, logo, heights = c(0.93, 0.1),
|
||||
composed <- arrangeGrob(plot, logo, heights = c(0.93, 0.1),
|
||||
padding = unit(0.1, "line"))
|
||||
}
|
||||
|
||||
# Wrap with a full-bleed background rect so any transparent areas (the
|
||||
# logo strip, padding gaps) pick up the plot's background color instead
|
||||
# of the device default (white).
|
||||
if (!is.null(bg_fill) && !is.na(bg_fill)) {
|
||||
bg_rect <- grid::rectGrob(gp = grid::gpar(fill = bg_fill, col = NA))
|
||||
grid::grobTree(bg_rect, composed)
|
||||
} else {
|
||||
composed
|
||||
}
|
||||
}
|
||||
|
||||
#' Measure a ggplot2 object caption
|
||||
@@ -84,8 +125,8 @@ add_logo <- function(plot, logo, margin_param = NULL) {
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
|
||||
#' measure_caption(p1) # Should equal 1 since no caption is required
|
||||
#' p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
|
||||
#' measure_caption(p1) # Should equal 1 since no caption is present
|
||||
measure_caption <- function(gg) {
|
||||
if (has_caption(gg)) {
|
||||
stringr::str_count(gg$labels$caption, pattern = "\n") + 1
|
||||
@@ -104,21 +145,23 @@ measure_caption <- function(gg) {
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
|
||||
#' p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
|
||||
#' has_caption(p1) # FALSE
|
||||
has_caption <- function(gg) {
|
||||
any(names(gg$labels) == "caption")
|
||||
}
|
||||
|
||||
#' Add a logo to a ggplot2 object
|
||||
#' Add a logo to multiple ggplot2 objects
|
||||
#'
|
||||
#' @param plot_list a list containing ggplot2 objects
|
||||
#' @param logo a grob containing the logo created with `make_logo_grob`
|
||||
#' @param nrow an integer, default = 1, for the number of rows to align the plots in
|
||||
#' @param widths an optional vector the same length as plot_list with the widths for each plot
|
||||
#' @param margin_param a number giving the adjustment up or down to help manually align logo and captions
|
||||
#' @param font_scale Numeric. Multiplicative scaling factor applied to text
|
||||
#' sizes before composing. Default `1.1`. See [add_logo()] for details.
|
||||
#'
|
||||
#' @return
|
||||
#' @return a grid object
|
||||
#' @note The resulting object needs to be drawn to the screen using grid.draw()
|
||||
#' @importFrom gridExtra arrangeGrob
|
||||
#' @importFrom ggplot2 theme
|
||||
@@ -126,18 +169,34 @@ has_caption <- function(gg) {
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(ggplot2); library(grid)
|
||||
#' tmp_plot <- ggplot(mtcars) + aes(x = hp, y = disp) + geom_point() + theme_civilytics()
|
||||
#' tmp_logo <- make_logo_grob()
|
||||
#' plot_and_logo <- add_logo(tmp_plot, tmp_logo)
|
||||
#' grid.draw(plot_and_logo)
|
||||
#' dev.off()
|
||||
add_logo_ga <- function(plot_list, logo, nrow = 1, widths = NULL, margin_param = NULL) {
|
||||
#' }
|
||||
add_logo_ga <- function(plot_list, logo, nrow = 1, widths = NULL,
|
||||
margin_param = NULL, font_scale = 1.1) {
|
||||
# Capture the background color from the first plot
|
||||
bg_fill <- plot_list[[1]]$theme$plot.background$fill
|
||||
|
||||
# Inflate text sizes to compensate for viewport shrinkage
|
||||
if (!is.null(font_scale) && font_scale != 1) {
|
||||
plot_list <- lapply(plot_list, function(p) {
|
||||
base_size <- p$theme$text$size %||% 14
|
||||
p + ggplot2::theme(
|
||||
text = ggplot2::element_text(size = base_size * font_scale)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
# Change position of logo depending on if plot has a caption
|
||||
if (!is.null(margin_param)) {
|
||||
margin <- theme(plot.margin = unit(c(7, 7, margin_param, 7), "pt"))
|
||||
} else if (any(unlist(lapply(plot_list, has_caption)))) {
|
||||
cap_lines <- measure_caption(plot_list[[1]]) # measure caption in first plot
|
||||
cap_lines <- max(sapply(plot_list, measure_caption))
|
||||
margin <- theme(plot.margin = unit(c(7, 7, -7 * sqrt(cap_lines), 7), "pt"))
|
||||
} else {
|
||||
margin <- theme(plot.margin = unit(c(7, 7, 7, 7), "pt"))
|
||||
@@ -150,29 +209,155 @@ add_logo_ga <- function(plot_list, logo, nrow = 1, widths = NULL, margin_param =
|
||||
}
|
||||
|
||||
if (nrow != 1) {
|
||||
hold <- arrangeGrob(grobs = plot_list, nrow = nrow, ncol = 1, widths = widths)
|
||||
if (!is.null(widths)) warning("`widths` is ignored when `nrow > 1`")
|
||||
hold <- arrangeGrob(grobs = plot_list, nrow = nrow, ncol = 1)
|
||||
} else {
|
||||
hold <- arrangeGrob(grobs = plot_list, nrow = 1, ncol = 2, widths = widths)
|
||||
hold <- arrangeGrob(grobs = plot_list, nrow = 1, ncol = length(plot_list), widths = widths)
|
||||
}
|
||||
|
||||
arrangeGrob(hold, logo, heights = c(0.93, .07))
|
||||
composed <- arrangeGrob(hold, logo, heights = c(0.93, .07))
|
||||
|
||||
if (!is.null(bg_fill) && !is.na(bg_fill)) {
|
||||
bg_rect <- grid::rectGrob(gp = grid::gpar(fill = bg_fill, col = NA))
|
||||
grid::grobTree(bg_rect, composed)
|
||||
} else {
|
||||
composed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' Get a Civilytics Logo grob
|
||||
#'
|
||||
#' @return a gg object which contains the logo file stored as a Grob suitable for manipulating in
|
||||
#' grid
|
||||
#' Returns a ggplot object containing the Civilytics logo as a rasterGrob,
|
||||
#' ready to compose with plots via [add_logo()], [add_logo_ga()], or the
|
||||
#' pipe-friendly [civilytics_logo()].
|
||||
#'
|
||||
#' @param type Character. `"wordmark"` (default) uses the full wordmark.
|
||||
#' `"mark"` uses the compact C-pulse icon only.
|
||||
#' @param variant Character. `"light"` (default) uses the dark logo for light
|
||||
#' backgrounds. `"dark"` uses the reverse (light) logo for dark backgrounds
|
||||
#' (pairs with [theme_civilytics_dark()]).
|
||||
#' @param position Character. Corner placement for the logo: `"bottom-right"`
|
||||
#' (default), `"bottom-left"`, `"top-right"`, or `"top-left"`. Controls
|
||||
#' horizontal alignment of the logo grob.
|
||||
#'
|
||||
#' @return A ggplot object (class `"gg"`) containing the logo grob.
|
||||
#' @export
|
||||
#' @import ggplot2
|
||||
#' @importFrom ggplot2 ggplot theme_void annotation_custom
|
||||
#' @examples
|
||||
#' logo <- make_logo_grob()
|
||||
#' class(logo) # gg
|
||||
make_logo_grob <- function() {
|
||||
logo_grob <- ggplot(mapping = aes(x = 0:1, y = 1)) +
|
||||
theme_void() +
|
||||
annotation_custom(get_png(system.file("img", "civilytics_logo.png",
|
||||
package="civilytics")), xmin= 0.7, xmax = 1)
|
||||
logo_grob
|
||||
#' logo <- make_logo_grob() # wordmark, light
|
||||
#' logo <- make_logo_grob("mark", "dark") # mark, dark
|
||||
#' logo <- make_logo_grob(position = "bottom-left") # left-aligned
|
||||
#' class(logo) # "gg" "ggplot"
|
||||
make_logo_grob <- function(type = c("wordmark", "mark"),
|
||||
variant = c("light", "dark"),
|
||||
position = c("bottom-right", "bottom-left",
|
||||
"top-right", "top-left")) {
|
||||
type <- match.arg(type)
|
||||
variant <- match.arg(variant)
|
||||
position <- match.arg(position)
|
||||
|
||||
img_file <- switch(
|
||||
paste(type, variant, sep = "_"),
|
||||
wordmark_light = "civilytics-wordmark.png",
|
||||
wordmark_dark = "civilytics-wordmark-reverse.png",
|
||||
mark_light = "civilytics-mark.png",
|
||||
mark_dark = "civilytics-mark-reverse.png"
|
||||
)
|
||||
|
||||
align_left <- grepl("left", position)
|
||||
|
||||
if (align_left) {
|
||||
xmin <- 0
|
||||
xmax <- if (type == "mark") 0.07 else 0.35
|
||||
} else {
|
||||
xmin <- if (type == "mark") 0.93 else 0.65
|
||||
xmax <- 1
|
||||
}
|
||||
|
||||
ggplot2::ggplot() +
|
||||
ggplot2::theme_void() +
|
||||
ggplot2::annotation_custom(
|
||||
get_png(system.file("img", img_file, package = "civilytics")),
|
||||
xmin = xmin, xmax = xmax
|
||||
)
|
||||
}
|
||||
|
||||
#' Add a Civilytics logo to a ggplot (pipe-friendly)
|
||||
#'
|
||||
#' A convenience wrapper that creates the logo grob and attaches it to a
|
||||
#' corner of the plot in one call. Designed for use with the base pipe `|>`.
|
||||
#'
|
||||
#' **Important:** R's `|>` has *higher* precedence than `+`, so you must
|
||||
#' wrap the ggplot chain in parentheses before piping:
|
||||
#'
|
||||
#' ```
|
||||
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||
#' geom_point() +
|
||||
#' theme_civilytics()) |>
|
||||
#' civilytics_logo()
|
||||
#' ```
|
||||
#'
|
||||
#' @param plot A ggplot object.
|
||||
#' @param type Character. `"wordmark"` (default) or `"mark"`. Passed to
|
||||
#' [make_logo_grob()].
|
||||
#' @param variant Character. `"light"` (default) or `"dark"`. Passed to
|
||||
#' [make_logo_grob()].
|
||||
#' @param position Character. Corner placement for the logo: `"bottom-right"`
|
||||
#' (default), `"bottom-left"`, `"top-right"`, or `"top-left"`.
|
||||
#' @param margin_param Numeric or `NULL`. Manual margin adjustment passed to
|
||||
#' [add_logo()].
|
||||
#' @param font_scale Numeric. Inflate text sizes by this factor to compensate
|
||||
#' for viewport shrinkage when composing with [gridExtra::arrangeGrob()].
|
||||
#' Default `1.1` (~10 \% inflation). Set to `1` to disable.
|
||||
#'
|
||||
#' @return A grob (from [gridExtra::arrangeGrob()]) ready to draw with
|
||||
#' [grid::grid.draw()].
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(ggplot2); library(grid)
|
||||
#'
|
||||
#' # Pipe usage — parentheses required around the ggplot chain
|
||||
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||
#' geom_point() +
|
||||
#' theme_civilytics()) |>
|
||||
#' civilytics_logo() |>
|
||||
#' grid.draw()
|
||||
#'
|
||||
#' # Top-right placement
|
||||
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||
#' geom_point() +
|
||||
#' theme_civilytics()) |>
|
||||
#' civilytics_logo(position = "top-right") |>
|
||||
#' grid.draw()
|
||||
#'
|
||||
#' # Bottom-left with mark
|
||||
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||
#' geom_point() +
|
||||
#' theme_civilytics()) |>
|
||||
#' civilytics_logo(type = "mark", position = "bottom-left") |>
|
||||
#' grid.draw()
|
||||
#'
|
||||
#' # Dark theme with mark in top-left
|
||||
#' (ggplot(mpg, aes(displ, hwy)) +
|
||||
#' geom_point() +
|
||||
#' theme_civilytics_dark()) |>
|
||||
#' civilytics_logo(variant = "dark", type = "mark",
|
||||
#' position = "top-left") |>
|
||||
#' grid.draw()
|
||||
#' }
|
||||
civilytics_logo <- function(plot,
|
||||
type = c("wordmark", "mark"),
|
||||
variant = c("light", "dark"),
|
||||
position = c("bottom-right", "bottom-left",
|
||||
"top-right", "top-left"),
|
||||
margin_param = NULL,
|
||||
font_scale = 1.1) {
|
||||
position <- match.arg(position)
|
||||
logo <- make_logo_grob(type = type, variant = variant, position = position)
|
||||
add_logo(plot, logo, margin_param = margin_param, font_scale = font_scale,
|
||||
position = position)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,193 @@
|
||||
#' Install Civilytics Quarto Themes and Templates
|
||||
#'
|
||||
#' Helper functions that copy Civilytics Quarto assets from the installed
|
||||
#' package into your Quarto project directory. Logos are stored in a single
|
||||
#' canonical location (`inst/img/`) and copied to the paths expected by each
|
||||
#' template at install time.
|
||||
#'
|
||||
#' @name quarto-helpers
|
||||
NULL
|
||||
|
||||
|
||||
# -- internal utilities -------------------------------------------------------
|
||||
|
||||
#' Copy a package file to a project directory
|
||||
#'
|
||||
#' @param src Relative path within the installed package (under `inst/`).
|
||||
#' @param dst Destination path relative to `path`.
|
||||
#' @param path Project root.
|
||||
#' @param force Overwrite existing files?
|
||||
#' @return Invisible logical indicating success.
|
||||
#' @keywords internal
|
||||
.copy_pkg_file <- function(src, dst, path, force) {
|
||||
from <- system.file(src, package = "civilytics", mustWork = TRUE)
|
||||
to <- file.path(path, dst)
|
||||
dir.create(dirname(to), recursive = TRUE, showWarnings = FALSE)
|
||||
|
||||
if (file.exists(to) && !force) {
|
||||
message(" skip: ", dst, " (already exists; use force = TRUE to overwrite)")
|
||||
return(invisible(FALSE))
|
||||
}
|
||||
|
||||
file.copy(from, to, overwrite = force)
|
||||
message(" copy: ", dst)
|
||||
invisible(TRUE)
|
||||
}
|
||||
|
||||
#' Copy logo SVGs from inst/img/ to a target directory
|
||||
#'
|
||||
#' @param dest_dir Destination directory relative to `path`.
|
||||
#' @param path Project root.
|
||||
#' @param force Overwrite existing files?
|
||||
#' @param files Character vector of logo filenames to copy.
|
||||
#' @keywords internal
|
||||
.copy_logos <- function(dest_dir, path, force,
|
||||
files = c("civilytics-mark.svg",
|
||||
"civilytics-mark-reverse.svg",
|
||||
"civilytics-wordmark.svg",
|
||||
"civilytics-wordmark-reverse.svg",
|
||||
"civilytics-pulse.svg")) {
|
||||
for (f in files) {
|
||||
.copy_pkg_file(file.path("img", f), file.path(dest_dir, f), path, force)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# -- public API ----------------------------------------------------------------
|
||||
|
||||
#' Install the Civilytics Reveal.js extension
|
||||
#'
|
||||
#' Copies the Civilytics Reveal.js Quarto extension into
|
||||
#' `_extensions/civilytics-reveal/` under `path`, including brand logos
|
||||
#' from the package. After running this function, add
|
||||
#' `format: civilytics-reveal-revealjs` to your `.qmd` YAML front-matter.
|
||||
#'
|
||||
#' @param path Character. Project directory to install into. Default `"."`.
|
||||
#' @param force Logical. Overwrite existing files? Default `FALSE`.
|
||||
#'
|
||||
#' @return Invisible `NULL`.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' use_civilytics_revealjs()
|
||||
#' }
|
||||
use_civilytics_revealjs <- function(path = ".", force = FALSE) {
|
||||
path <- normalizePath(path, mustWork = TRUE)
|
||||
message("Installing Civilytics Reveal.js extension into: ", path)
|
||||
|
||||
ext_src <- "quarto/extensions/civilytics-reveal"
|
||||
ext_dst <- "_extensions/civilytics-reveal"
|
||||
|
||||
ext_files <- c(
|
||||
"_extension.yml",
|
||||
"civilytics.scss",
|
||||
"civilytics.css",
|
||||
"civilytics-head.html",
|
||||
"civilytics-after.html"
|
||||
)
|
||||
for (f in ext_files) {
|
||||
.copy_pkg_file(file.path(ext_src, f), file.path(ext_dst, f), path, force)
|
||||
}
|
||||
|
||||
# Logos — single source from inst/img/
|
||||
.copy_logos(file.path(ext_dst, "assets"), path, force)
|
||||
|
||||
message("\nDone! Add this to your .qmd front-matter:\n")
|
||||
message("---")
|
||||
message("format: civilytics-reveal-revealjs")
|
||||
message("---")
|
||||
invisible(NULL)
|
||||
}
|
||||
|
||||
|
||||
#' Install the Civilytics document theme
|
||||
#'
|
||||
#' Copies Civilytics HTML, PDF (LaTeX), and Typst theme files into
|
||||
#' your Quarto project. Also installs `_brand.yml` and the logo
|
||||
#' assets it references.
|
||||
#'
|
||||
#' @inheritParams use_civilytics_revealjs
|
||||
#'
|
||||
#' @return Invisible `NULL`.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' use_civilytics_theme()
|
||||
#' }
|
||||
use_civilytics_theme <- function(path = ".", force = FALSE) {
|
||||
path <- normalizePath(path, mustWork = TRUE)
|
||||
message("Installing Civilytics document theme into: ", path)
|
||||
|
||||
# Brand file
|
||||
.copy_pkg_file("quarto/_brand.yml", "_brand.yml", path, force)
|
||||
|
||||
# HTML theme
|
||||
theme_files <- c("civilytics.scss", "_tokens.scss", "extras.css")
|
||||
for (f in theme_files) {
|
||||
.copy_pkg_file(file.path("quarto/theme", f), file.path("theme", f), path, force)
|
||||
}
|
||||
|
||||
# LaTeX
|
||||
latex_files <- c("civilytics.tex", "civilytics-title.tex")
|
||||
for (f in latex_files) {
|
||||
.copy_pkg_file(file.path("quarto/latex", f), file.path("latex", f), path, force)
|
||||
}
|
||||
|
||||
# Typst
|
||||
.copy_pkg_file(
|
||||
"quarto/typst/civilytics-typst.typ",
|
||||
"typst/civilytics-typst.typ",
|
||||
path, force
|
||||
)
|
||||
|
||||
# Logos — for _brand.yml (expects assets/logo/)
|
||||
.copy_logos("assets/logo", path, force)
|
||||
|
||||
# Example report
|
||||
.copy_pkg_file("quarto/examples/report.qmd", "examples/report.qmd", path, force)
|
||||
|
||||
message("\nDone! Example YAML for a report:\n")
|
||||
message("---")
|
||||
message("format:")
|
||||
message(" html:")
|
||||
message(" theme: theme/civilytics.scss")
|
||||
message(" css: theme/extras.css")
|
||||
message(" pdf:")
|
||||
message(" include-in-header: latex/civilytics.tex")
|
||||
message(" include-before-body: latex/civilytics-title.tex")
|
||||
message(" typst:")
|
||||
message(" template: typst/civilytics-typst.typ")
|
||||
message("---")
|
||||
message("\nSee examples/report.qmd for a complete example.")
|
||||
invisible(NULL)
|
||||
}
|
||||
|
||||
|
||||
#' Install the Civilytics brand file only
|
||||
#'
|
||||
#' Copies `_brand.yml` and the logo assets it references into your
|
||||
#' Quarto project. This gives you Quarto 1.5+ automatic brand
|
||||
#' styling (colors, fonts, logos) without the full theme SCSS.
|
||||
#'
|
||||
#' @inheritParams use_civilytics_revealjs
|
||||
#'
|
||||
#' @return Invisible `NULL`.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' use_civilytics_brand()
|
||||
#' }
|
||||
use_civilytics_brand <- function(path = ".", force = FALSE) {
|
||||
path <- normalizePath(path, mustWork = TRUE)
|
||||
message("Installing Civilytics brand file into: ", path)
|
||||
|
||||
.copy_pkg_file("quarto/_brand.yml", "_brand.yml", path, force)
|
||||
.copy_logos("assets/logo", path, force)
|
||||
|
||||
message("\nDone! Quarto 1.5+ will auto-apply brand colors, fonts, and logos.")
|
||||
message("See: https://quarto.org/docs/authoring/brand.html")
|
||||
invisible(NULL)
|
||||
}
|
||||
@@ -1,168 +1,622 @@
|
||||
#' Make the Civilytics plot theme
|
||||
#' Civilytics ggplot2 theme
|
||||
#'
|
||||
#' @param font_size default 14, a number representing the base font for the theme
|
||||
#' @param font_family default "", a character for the font family to use in the theme
|
||||
#' @param line_size default 0.5, the line size to use for the theme
|
||||
#' @param rel_small default 12/14, the scale factor to create a small font from the base font_size
|
||||
#' @param rel_tiny default 11/14, the scale factor to create a tiny font from the base font_size
|
||||
#' @param rel_large default 16/14, the scale factor to create a large font from the base font_size
|
||||
#' @importFrom graphics plot
|
||||
#' @return a ggplot2 theme object suitable for combining with ggplot objects to theme them
|
||||
#' A complete ggplot2 theme built on [ggplot2::theme_grey()] using the
|
||||
#' Civilytics brand color palette and typography. Requires ggplot2 >= 4.0.0
|
||||
#' for the `ink`, `paper`, and `accent` base-theme parameters.
|
||||
#'
|
||||
#' Produces an editorial, Pew-style layout: visible x-axis line to ground
|
||||
#' the data, light horizontal gridlines for reference, no panel border or
|
||||
#' y-axis line. Plot title and caption are left-aligned to the full plot
|
||||
#' region.
|
||||
#'
|
||||
#' Brand fonts (Inter for UI text, Libre Franklin for titles) are loaded
|
||||
#' automatically via [showtext] when the package is attached. Call
|
||||
#' [civilytics_load_fonts()] to reload them if needed.
|
||||
#'
|
||||
#' @param font_size Numeric. Base font size in points. Default `14`.
|
||||
#' @param font_family Character. Font family for body/axis text. Default
|
||||
#' `"Inter"` (loaded via showtext).
|
||||
#' @param title_family Character. Font family for plot titles and strip labels.
|
||||
#' Default `"Libre Franklin"` (loaded via showtext).
|
||||
#' @param line_size Numeric. Base line width. Default `0.5`.
|
||||
#' @param rel_small Numeric. Scale factor for small text relative to
|
||||
#' `font_size`. Default `12/14`.
|
||||
#' @param rel_tiny Numeric. Scale factor for tiny text relative to `font_size`.
|
||||
#' Default `11/14`.
|
||||
#' @param rel_large Numeric. Scale factor for large text (titles) relative to
|
||||
#' `font_size`. Default `20/14` (~1.43x), matching the Civilytics editorial
|
||||
#' design system.
|
||||
#' @param ink Character. Hex code for foreground/text color. Defaults to
|
||||
#' [civilytics_colors]`["ink"]` (`#0E1A2B`).
|
||||
#' @param paper Character. Hex code for background color. Defaults to
|
||||
#' [civilytics_colors]`["paper"]` (`#FAF7F2`).
|
||||
#' @param accent Character. Hex code for accent/highlight color. Defaults to
|
||||
#' [civilytics_colors]`["ember_600"]` (`#C25311`).
|
||||
#' @param strip_color Character. Hex code for facet strip background. Defaults
|
||||
#' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).
|
||||
#' @param grid Character. Which major gridlines to draw: `"y"` (default,
|
||||
#' horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.
|
||||
#' @param paper_bg Logical. If `TRUE` (default), fill the plot and panel
|
||||
#' backgrounds with the warm `paper` color. Set to `FALSE` for a
|
||||
#' transparent background (useful for slides or overlay on colored
|
||||
#' surfaces).
|
||||
#'
|
||||
#' @section Font size hierarchy:
|
||||
#' All text sizes are derived from `font_size` using relative scale factors.
|
||||
#' At the default `font_size = 14`:
|
||||
#'
|
||||
#' | Element | Scale factor | Default size |
|
||||
#' |:--------|:-------------|:-------------|
|
||||
#' | Plot title | `rel_large` (1.43x) | ~20 pt |
|
||||
#' | Subtitle | 1.0x | 14 pt |
|
||||
#' | Axis text (tick labels) | `rel_small` (0.86x) | ~12 pt |
|
||||
#' | Axis titles | `rel_small` (0.86x) | ~12 pt |
|
||||
#' | Legend text | `rel_small` (0.86x) | ~12 pt |
|
||||
#' | Caption | `rel_tiny` (0.79x) | ~11 pt |
|
||||
#' | Legend title | `rel_tiny` (0.79x) | ~11 pt |
|
||||
#' | Strip text (facets) | `rel_small` (0.86x) | ~12 pt |
|
||||
#'
|
||||
#' To uniformly scale all text, change `font_size`. To adjust only the title
|
||||
#' prominence, change `rel_large`. When using [civilytics_logo()] to add a
|
||||
#' logo below the plot, pass `font_scale` to compensate for viewport
|
||||
#' shrinkage.
|
||||
#'
|
||||
#' @return A complete ggplot2 [ggplot2::theme()] object.
|
||||
#' @export
|
||||
theme_civilytics <-
|
||||
function (font_size = 14,
|
||||
font_family = "",
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(ggplot2)
|
||||
#'
|
||||
#' # Default editorial theme
|
||||
#' ggplot(mpg, aes(displ, hwy)) +
|
||||
#' geom_point() +
|
||||
#' theme_civilytics()
|
||||
#'
|
||||
#' # With both gridlines and brand colors
|
||||
#' ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||
#' geom_point() +
|
||||
#' scale_color_civilytics() +
|
||||
#' theme_civilytics(grid = "both")
|
||||
#'
|
||||
#' # Transparent background for embedding
|
||||
#' ggplot(mpg, aes(displ, hwy)) +
|
||||
#' geom_point() +
|
||||
#' theme_civilytics(paper_bg = FALSE)
|
||||
#'
|
||||
#' # Larger text for poster or display
|
||||
#' ggplot(mpg, aes(displ, hwy)) +
|
||||
#' geom_point() +
|
||||
#' theme_civilytics(font_size = 18)
|
||||
#' }
|
||||
theme_civilytics <- function(
|
||||
font_size = 14,
|
||||
font_family = CV_FONT_SANS,
|
||||
title_family = CV_FONT_DISPLAY,
|
||||
line_size = 0.5,
|
||||
rel_small = 12 / 14,
|
||||
rel_tiny = 11 / 14,
|
||||
rel_large = 16 / 14) {
|
||||
rel_large = 20 / 14,
|
||||
ink = unname(civilytics_colors["ink"]),
|
||||
paper = unname(civilytics_colors["paper"]),
|
||||
accent = unname(civilytics_colors["ember_600"]),
|
||||
strip_color = unname(civilytics_colors["paper_2"]),
|
||||
grid = c("y", "x", "both", "none"),
|
||||
paper_bg = TRUE) {
|
||||
|
||||
grid <- match.arg(grid)
|
||||
half_line <- font_size / 2
|
||||
small_size <- rel_small * font_size
|
||||
theme_grey(base_size = font_size, base_family = font_family) %+replace%
|
||||
theme(
|
||||
line = element_line(
|
||||
color = "black",
|
||||
size = line_size,
|
||||
rule_color <- unname(civilytics_colors["rule"])
|
||||
ink_2 <- unname(civilytics_colors["ink_2"])
|
||||
ink_3 <- unname(civilytics_colors["ink_3"])
|
||||
bg_color <- if (isTRUE(paper_bg)) paper else NA
|
||||
|
||||
# Grid line elements
|
||||
grid_line <- ggplot2::element_line(color = rule_color, linewidth = 0.35)
|
||||
no_line <- ggplot2::element_blank()
|
||||
|
||||
ggplot2::theme_grey(
|
||||
base_size = font_size,
|
||||
base_family = font_family,
|
||||
ink = ink,
|
||||
paper = paper,
|
||||
accent = accent
|
||||
) %+replace%
|
||||
ggplot2::theme(
|
||||
line = ggplot2::element_line(
|
||||
color = ink,
|
||||
linewidth = line_size,
|
||||
linetype = 1,
|
||||
lineend = "butt"
|
||||
),
|
||||
rect = element_rect(
|
||||
rect = ggplot2::element_rect(
|
||||
fill = NA,
|
||||
color = NA,
|
||||
size = line_size,
|
||||
linewidth = line_size,
|
||||
linetype = 1
|
||||
),
|
||||
text = element_text(
|
||||
text = ggplot2::element_text(
|
||||
family = font_family,
|
||||
face = "plain",
|
||||
color = "black",
|
||||
color = ink,
|
||||
size = font_size,
|
||||
hjust = 0.5,
|
||||
vjust = 0.5,
|
||||
angle = 0,
|
||||
lineheight = 0.9,
|
||||
margin = margin(),
|
||||
margin = ggplot2::margin(),
|
||||
debug = FALSE
|
||||
),
|
||||
axis.line = element_line(
|
||||
color = "black",
|
||||
size = line_size,
|
||||
# -- Axes --
|
||||
axis.line = ggplot2::element_blank(),
|
||||
axis.line.x = ggplot2::element_line(
|
||||
color = ink,
|
||||
linewidth = 0.6,
|
||||
lineend = "square"
|
||||
),
|
||||
axis.line.x = NULL,
|
||||
axis.line.y = NULL,
|
||||
axis.text = element_text(color = "black",
|
||||
size = small_size),
|
||||
axis.text.x = element_text(margin = margin(t = small_size / 4),
|
||||
vjust = 1),
|
||||
axis.text.x.top = element_text(margin = margin(b = small_size / 4),
|
||||
vjust = 0),
|
||||
axis.text.y = element_text(margin = margin(r = small_size / 4),
|
||||
hjust = 1),
|
||||
axis.text.y.right = element_text(margin = margin(l = small_size / 4),
|
||||
hjust = 0),
|
||||
axis.ticks = element_line(color = "black",
|
||||
size = line_size),
|
||||
axis.ticks.length = unit(half_line / 2,
|
||||
"pt"),
|
||||
axis.title.x = element_text(margin = margin(t = half_line / 2),
|
||||
vjust = 1),
|
||||
axis.title.x.top = element_text(margin = margin(b = half_line / 2),
|
||||
vjust = 0),
|
||||
axis.title.y = element_text(
|
||||
angle = 90,
|
||||
margin = margin(r = half_line /
|
||||
2),
|
||||
axis.line.y = ggplot2::element_blank(),
|
||||
axis.text = ggplot2::element_text(
|
||||
color = ink_2,
|
||||
size = ggplot2::rel(rel_small)
|
||||
),
|
||||
axis.text.x = ggplot2::element_text(
|
||||
margin = ggplot2::margin(t = small_size / 4),
|
||||
vjust = 1
|
||||
),
|
||||
axis.title.y.right = element_text(
|
||||
angle = -90,
|
||||
margin = margin(l = half_line / 2),
|
||||
axis.text.x.top = ggplot2::element_text(
|
||||
margin = ggplot2::margin(b = small_size / 4),
|
||||
vjust = 0
|
||||
),
|
||||
legend.background = element_blank(),
|
||||
legend.spacing = unit(font_size, "pt"),
|
||||
axis.text.y = ggplot2::element_text(
|
||||
margin = ggplot2::margin(r = small_size / 4),
|
||||
hjust = 1
|
||||
),
|
||||
axis.text.y.right = ggplot2::element_text(
|
||||
margin = ggplot2::margin(l = small_size / 4),
|
||||
hjust = 0
|
||||
),
|
||||
axis.ticks = ggplot2::element_line(
|
||||
color = ink_3,
|
||||
linewidth = 0.4
|
||||
),
|
||||
axis.ticks.length = ggplot2::unit(4, "pt"),
|
||||
axis.title.x = ggplot2::element_text(
|
||||
size = ggplot2::rel(rel_small),
|
||||
color = ink_3,
|
||||
margin = ggplot2::margin(t = 10),
|
||||
vjust = 1
|
||||
),
|
||||
axis.title.x.top = ggplot2::element_text(
|
||||
size = ggplot2::rel(rel_small),
|
||||
color = ink_3,
|
||||
margin = ggplot2::margin(b = half_line / 2),
|
||||
vjust = 0
|
||||
),
|
||||
axis.title.y = ggplot2::element_text(
|
||||
size = ggplot2::rel(rel_small),
|
||||
color = ink_3,
|
||||
angle = 90,
|
||||
margin = ggplot2::margin(r = 10),
|
||||
vjust = 1
|
||||
),
|
||||
axis.title.y.right = ggplot2::element_text(
|
||||
size = ggplot2::rel(rel_small),
|
||||
color = ink_3,
|
||||
angle = -90,
|
||||
margin = ggplot2::margin(l = half_line / 2),
|
||||
vjust = 0
|
||||
),
|
||||
# -- Legend --
|
||||
legend.background = ggplot2::element_blank(),
|
||||
legend.spacing = ggplot2::unit(font_size, "pt"),
|
||||
legend.spacing.x = NULL,
|
||||
legend.spacing.y = NULL,
|
||||
legend.margin = margin(0,
|
||||
0, 0, 0),
|
||||
legend.key = element_blank(),
|
||||
legend.key.size = unit(1.1 *
|
||||
font_size, "pt"),
|
||||
legend.margin = ggplot2::margin(0, 0, 4, 0),
|
||||
legend.key = ggplot2::element_blank(),
|
||||
legend.key.size = ggplot2::unit(12, "pt"),
|
||||
legend.key.height = NULL,
|
||||
legend.key.width = NULL,
|
||||
legend.text = element_text(size = rel(rel_small)),
|
||||
legend.text.align = NULL,
|
||||
legend.title = element_text(hjust = 0),
|
||||
legend.title.align = NULL,
|
||||
legend.position = "right",
|
||||
legend.text = ggplot2::element_text(
|
||||
size = ggplot2::rel(rel_small),
|
||||
color = ink_2
|
||||
),
|
||||
legend.title = ggplot2::element_text(
|
||||
hjust = 0,
|
||||
face = "bold",
|
||||
size = ggplot2::rel(rel_tiny),
|
||||
color = ink_3
|
||||
),
|
||||
legend.position = "top",
|
||||
legend.direction = NULL,
|
||||
legend.justification = c("left",
|
||||
"center"),
|
||||
legend.justification = c("left", "center"),
|
||||
legend.box = NULL,
|
||||
legend.box.margin = margin(0,
|
||||
0, 0, 0),
|
||||
legend.box.background = element_blank(),
|
||||
legend.box.spacing = unit(font_size, "pt"),
|
||||
panel.background = element_blank(),
|
||||
panel.border = element_blank(),
|
||||
panel.grid = element_blank(),
|
||||
panel.grid.major = NULL,
|
||||
panel.grid.minor = NULL,
|
||||
panel.grid.major.x = NULL,
|
||||
panel.grid.major.y = NULL,
|
||||
panel.grid.minor.x = NULL,
|
||||
panel.grid.minor.y = NULL,
|
||||
panel.spacing = unit(half_line,
|
||||
"pt"),
|
||||
legend.box.margin = ggplot2::margin(0, 0, 0, 0),
|
||||
legend.box.background = ggplot2::element_blank(),
|
||||
legend.box.spacing = ggplot2::unit(font_size, "pt"),
|
||||
# -- Panel --
|
||||
panel.background = ggplot2::element_rect(fill = bg_color, color = NA),
|
||||
panel.border = ggplot2::element_blank(),
|
||||
panel.grid.minor = ggplot2::element_blank(),
|
||||
panel.grid.major.x = if (grid %in% c("x", "both")) grid_line else no_line,
|
||||
panel.grid.major.y = if (grid %in% c("y", "both")) grid_line else no_line,
|
||||
panel.spacing = ggplot2::unit(16, "pt"),
|
||||
panel.spacing.x = NULL,
|
||||
panel.spacing.y = NULL,
|
||||
panel.ontop = FALSE,
|
||||
strip.background = element_rect(fill = "grey80"),
|
||||
strip.text = element_text(
|
||||
size = rel(rel_small),
|
||||
margin = margin(half_line / 2, half_line /
|
||||
2, half_line / 2,
|
||||
half_line / 2)
|
||||
# -- Facet strips --
|
||||
strip.background = ggplot2::element_rect(fill = strip_color, color = NA),
|
||||
strip.text = ggplot2::element_text(
|
||||
family = font_family,
|
||||
face = "bold",
|
||||
size = ggplot2::rel(rel_small),
|
||||
color = ink,
|
||||
margin = ggplot2::margin(
|
||||
half_line / 2, half_line / 2,
|
||||
half_line / 2, half_line / 2
|
||||
)
|
||||
),
|
||||
strip.text.x = NULL,
|
||||
strip.text.y = element_text(angle = -90),
|
||||
strip.text.y = ggplot2::element_text(angle = -90),
|
||||
strip.placement = "inside",
|
||||
strip.placement.x = NULL,
|
||||
strip.placement.y = NULL,
|
||||
strip.switch.pad.grid = unit(half_line / 2,
|
||||
"pt"),
|
||||
strip.switch.pad.wrap = unit(half_line / 2,
|
||||
"pt"),
|
||||
plot.background = element_blank(),
|
||||
plot.title = element_text(
|
||||
strip.switch.pad.grid = ggplot2::unit(half_line / 2, "pt"),
|
||||
strip.switch.pad.wrap = ggplot2::unit(half_line / 2, "pt"),
|
||||
# -- Plot-level --
|
||||
plot.background = ggplot2::element_rect(fill = bg_color, color = NA),
|
||||
plot.title = ggplot2::element_text(
|
||||
family = title_family,
|
||||
face = "bold",
|
||||
size = rel(rel_large),
|
||||
size = ggplot2::rel(rel_large),
|
||||
hjust = 0,
|
||||
vjust = 1,
|
||||
margin = margin(b = half_line)
|
||||
margin = ggplot2::margin(b = 4)
|
||||
),
|
||||
plot.subtitle = element_text(
|
||||
size = rel(rel_small),
|
||||
plot.title.position = "plot",
|
||||
plot.subtitle = ggplot2::element_text(
|
||||
size = ggplot2::rel(1),
|
||||
color = ink_2,
|
||||
hjust = 0,
|
||||
vjust = 1,
|
||||
margin = margin(b = half_line)
|
||||
lineheight = 1.3,
|
||||
margin = ggplot2::margin(b = 14)
|
||||
),
|
||||
plot.caption = element_text(
|
||||
size = rel(rel_tiny),
|
||||
hjust = 0, # set hjust to 0
|
||||
plot.caption = ggplot2::element_text(
|
||||
size = ggplot2::rel(rel_tiny),
|
||||
color = ink_3,
|
||||
hjust = 0,
|
||||
vjust = 1,
|
||||
lineheight = 1,
|
||||
margin = margin(t = half_line)
|
||||
lineheight = 1.3,
|
||||
margin = ggplot2::margin(t = 14)
|
||||
),
|
||||
plot.tag = element_text(
|
||||
plot.caption.position = "plot",
|
||||
plot.tag = ggplot2::element_text(
|
||||
face = "bold",
|
||||
color = accent,
|
||||
size = ggplot2::rel(rel_tiny),
|
||||
hjust = 0,
|
||||
vjust = 0.7
|
||||
),
|
||||
plot.tag.position = c(0, 1),
|
||||
plot.margin = margin(half_line,
|
||||
half_line, half_line, half_line),
|
||||
plot.margin = ggplot2::margin(16, 18, 16, 16),
|
||||
complete = TRUE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' Dark variant of the Civilytics ggplot2 theme
|
||||
#'
|
||||
#' Convenience wrapper around [theme_civilytics()] with dark-background
|
||||
#' defaults: navy (`#1A2E4A`) paper, warm off-white (`#FAF7F2`) ink, and
|
||||
#' a lighter ember accent (`#E07840`). Facet strips use primary navy.
|
||||
#'
|
||||
#' Pair with `make_logo_grob(variant = "dark")` for the white logo.
|
||||
#'
|
||||
#' @inheritParams theme_civilytics
|
||||
#'
|
||||
#' @return A complete ggplot2 [ggplot2::theme()] object.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(ggplot2)
|
||||
#' ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||
#' geom_point() +
|
||||
#' scale_color_civilytics() +
|
||||
#' theme_civilytics_dark()
|
||||
#' }
|
||||
theme_civilytics_dark <- function(
|
||||
font_size = 14,
|
||||
font_family = CV_FONT_SANS,
|
||||
title_family = CV_FONT_DISPLAY,
|
||||
line_size = 0.5,
|
||||
rel_small = 12 / 14,
|
||||
rel_tiny = 11 / 14,
|
||||
rel_large = 20 / 14,
|
||||
ink = unname(civilytics_colors["paper"]),
|
||||
paper = unname(civilytics_colors["navy_700"]),
|
||||
accent = unname(civilytics_colors["ember_400"]),
|
||||
strip_color = unname(civilytics_colors["navy_600"]),
|
||||
grid = c("y", "x", "both", "none"),
|
||||
paper_bg = TRUE) {
|
||||
|
||||
theme_civilytics(
|
||||
font_size = font_size,
|
||||
font_family = font_family,
|
||||
title_family = title_family,
|
||||
line_size = line_size,
|
||||
rel_small = rel_small,
|
||||
rel_tiny = rel_tiny,
|
||||
rel_large = rel_large,
|
||||
ink = ink,
|
||||
paper = paper,
|
||||
accent = accent,
|
||||
strip_color = strip_color,
|
||||
grid = grid,
|
||||
paper_bg = paper_bg
|
||||
) +
|
||||
# The base theme hardcodes ink_2/ink_3 for subtitle/caption, which are
|
||||
# dark colors meant for light backgrounds. Override with lighter values
|
||||
# so text remains readable on the navy background.
|
||||
ggplot2::theme(
|
||||
plot.subtitle = ggplot2::element_text(
|
||||
color = unname(civilytics_colors["navy_200"])
|
||||
),
|
||||
plot.caption = ggplot2::element_text(
|
||||
color = unname(civilytics_colors["navy_300"])
|
||||
),
|
||||
axis.text = ggplot2::element_text(
|
||||
color = unname(civilytics_colors["navy_200"])
|
||||
),
|
||||
axis.title.x = ggplot2::element_text(
|
||||
color = unname(civilytics_colors["navy_300"])
|
||||
),
|
||||
axis.title.y = ggplot2::element_text(
|
||||
color = unname(civilytics_colors["navy_300"])
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' Slide-friendly Civilytics ggplot2 theme
|
||||
#'
|
||||
#' Variant of [theme_civilytics()] sized for Reveal.js slides or PowerPoint
|
||||
#' exports: larger base font (18pt), transparent background, wider margins,
|
||||
#' and a heavier x-axis line. Gridlines default to horizontal only.
|
||||
#'
|
||||
#' @inheritParams theme_civilytics
|
||||
#'
|
||||
#' @return A complete ggplot2 [ggplot2::theme()] object.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(ggplot2)
|
||||
#' ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||
#' geom_point() +
|
||||
#' scale_color_civilytics() +
|
||||
#' theme_civilytics_slide()
|
||||
#' }
|
||||
theme_civilytics_slide <- function(
|
||||
font_size = 18,
|
||||
font_family = CV_FONT_SANS,
|
||||
title_family = CV_FONT_DISPLAY,
|
||||
line_size = 0.5,
|
||||
rel_small = 12 / 14,
|
||||
rel_tiny = 11 / 14,
|
||||
rel_large = 20 / 14,
|
||||
ink = unname(civilytics_colors["ink"]),
|
||||
paper = unname(civilytics_colors["paper"]),
|
||||
accent = unname(civilytics_colors["ember_600"]),
|
||||
strip_color = unname(civilytics_colors["paper_2"]),
|
||||
grid = c("y", "x", "both", "none"),
|
||||
paper_bg = FALSE) {
|
||||
|
||||
theme_civilytics(
|
||||
font_size = font_size,
|
||||
font_family = font_family,
|
||||
title_family = title_family,
|
||||
line_size = line_size,
|
||||
rel_small = rel_small,
|
||||
rel_tiny = rel_tiny,
|
||||
rel_large = rel_large,
|
||||
ink = ink,
|
||||
paper = paper,
|
||||
accent = accent,
|
||||
strip_color = strip_color,
|
||||
grid = grid,
|
||||
paper_bg = paper_bg
|
||||
) +
|
||||
ggplot2::theme(
|
||||
axis.line.x = ggplot2::element_line(
|
||||
color = ink,
|
||||
linewidth = 0.8,
|
||||
lineend = "square"
|
||||
),
|
||||
plot.margin = ggplot2::margin(24, 24, 24, 24)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# -- Map themes ----------------------------------------------------------------
|
||||
|
||||
#' Shared map-theme overrides
|
||||
#'
|
||||
#' Strips away axes, ticks, gridlines, and axis titles/labels — the elements
|
||||
#' that are meaningless on a choropleth or spatial plot.
|
||||
#'
|
||||
#' @return A partial ggplot2 [ggplot2::theme()] object.
|
||||
#' @keywords internal
|
||||
.map_theme_extras <- function() {
|
||||
ggplot2::theme(
|
||||
axis.line = ggplot2::element_blank(),
|
||||
axis.line.x = ggplot2::element_blank(),
|
||||
axis.line.y = ggplot2::element_blank(),
|
||||
axis.text = ggplot2::element_blank(),
|
||||
axis.text.x = ggplot2::element_blank(),
|
||||
axis.text.y = ggplot2::element_blank(),
|
||||
axis.ticks = ggplot2::element_blank(),
|
||||
axis.ticks.length = ggplot2::unit(0, "pt"),
|
||||
axis.title.x = ggplot2::element_blank(),
|
||||
axis.title.y = ggplot2::element_blank(),
|
||||
panel.grid.major.x = ggplot2::element_blank(),
|
||||
panel.grid.major.y = ggplot2::element_blank(),
|
||||
panel.grid.minor = ggplot2::element_blank()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' Map-friendly Civilytics ggplot2 theme
|
||||
#'
|
||||
#' Variant of [theme_civilytics()] for choropleths and spatial plots.
|
||||
#' Suppresses axes, axis labels, ticks, and gridlines while keeping the
|
||||
#' Civilytics brand typography, colors, and plot-level elements (title,
|
||||
#' subtitle, caption, legend).
|
||||
#'
|
||||
#' @inheritParams theme_civilytics
|
||||
#'
|
||||
#' @return A complete ggplot2 [ggplot2::theme()] object.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(ggplot2)
|
||||
#' # With sf data:
|
||||
#' ggplot(map_data) +
|
||||
#' geom_sf(aes(fill = value)) +
|
||||
#' scale_fill_civilytics_c("seq_navy") +
|
||||
#' theme_civilytics_map()
|
||||
#' }
|
||||
theme_civilytics_map <- function(
|
||||
font_size = 14,
|
||||
font_family = CV_FONT_SANS,
|
||||
title_family = CV_FONT_DISPLAY,
|
||||
line_size = 0.5,
|
||||
rel_small = 12 / 14,
|
||||
rel_tiny = 11 / 14,
|
||||
rel_large = 20 / 14,
|
||||
ink = unname(civilytics_colors["ink"]),
|
||||
paper = unname(civilytics_colors["paper"]),
|
||||
accent = unname(civilytics_colors["ember_600"]),
|
||||
strip_color = unname(civilytics_colors["paper_2"]),
|
||||
paper_bg = TRUE) {
|
||||
|
||||
theme_civilytics(
|
||||
font_size = font_size,
|
||||
font_family = font_family,
|
||||
title_family = title_family,
|
||||
line_size = line_size,
|
||||
rel_small = rel_small,
|
||||
rel_tiny = rel_tiny,
|
||||
rel_large = rel_large,
|
||||
ink = ink,
|
||||
paper = paper,
|
||||
accent = accent,
|
||||
strip_color = strip_color,
|
||||
grid = "none",
|
||||
paper_bg = paper_bg
|
||||
) + .map_theme_extras()
|
||||
}
|
||||
|
||||
|
||||
#' Dark map-friendly Civilytics ggplot2 theme
|
||||
#'
|
||||
#' Variant of [theme_civilytics_dark()] for choropleths and spatial plots.
|
||||
#' Suppresses axes, axis labels, ticks, and gridlines on a dark navy
|
||||
#' background. Pair with `make_logo_grob(variant = "dark")`.
|
||||
#'
|
||||
#' @inheritParams theme_civilytics_dark
|
||||
#'
|
||||
#' @return A complete ggplot2 [ggplot2::theme()] object.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(ggplot2)
|
||||
#' ggplot(map_data) +
|
||||
#' geom_sf(aes(fill = value)) +
|
||||
#' scale_fill_civilytics_c("seq_ember") +
|
||||
#' theme_civilytics_dark_map()
|
||||
#' }
|
||||
theme_civilytics_dark_map <- function(
|
||||
font_size = 14,
|
||||
font_family = CV_FONT_SANS,
|
||||
title_family = CV_FONT_DISPLAY,
|
||||
line_size = 0.5,
|
||||
rel_small = 12 / 14,
|
||||
rel_tiny = 11 / 14,
|
||||
rel_large = 20 / 14,
|
||||
ink = unname(civilytics_colors["paper"]),
|
||||
paper = unname(civilytics_colors["navy_700"]),
|
||||
accent = unname(civilytics_colors["ember_400"]),
|
||||
strip_color = unname(civilytics_colors["navy_600"]),
|
||||
paper_bg = TRUE) {
|
||||
|
||||
theme_civilytics_dark(
|
||||
font_size = font_size,
|
||||
font_family = font_family,
|
||||
title_family = title_family,
|
||||
line_size = line_size,
|
||||
rel_small = rel_small,
|
||||
rel_tiny = rel_tiny,
|
||||
rel_large = rel_large,
|
||||
ink = ink,
|
||||
paper = paper,
|
||||
accent = accent,
|
||||
strip_color = strip_color,
|
||||
grid = "none",
|
||||
paper_bg = paper_bg
|
||||
) + .map_theme_extras()
|
||||
}
|
||||
|
||||
|
||||
#' Slide-friendly map Civilytics ggplot2 theme
|
||||
#'
|
||||
#' Variant of [theme_civilytics_slide()] for choropleths and spatial plots
|
||||
#' on slides. Combines the larger base font and transparent background of
|
||||
#' the slide theme with suppressed axes, ticks, and gridlines.
|
||||
#'
|
||||
#' @inheritParams theme_civilytics_slide
|
||||
#'
|
||||
#' @return A complete ggplot2 [ggplot2::theme()] object.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(ggplot2)
|
||||
#' ggplot(map_data) +
|
||||
#' geom_sf(aes(fill = value)) +
|
||||
#' scale_fill_civilytics_c("seq_navy") +
|
||||
#' theme_civilytics_slide_map()
|
||||
#' }
|
||||
theme_civilytics_slide_map <- function(
|
||||
font_size = 18,
|
||||
font_family = CV_FONT_SANS,
|
||||
title_family = CV_FONT_DISPLAY,
|
||||
line_size = 0.5,
|
||||
rel_small = 12 / 14,
|
||||
rel_tiny = 11 / 14,
|
||||
rel_large = 20 / 14,
|
||||
ink = unname(civilytics_colors["ink"]),
|
||||
paper = unname(civilytics_colors["paper"]),
|
||||
accent = unname(civilytics_colors["ember_600"]),
|
||||
strip_color = unname(civilytics_colors["paper_2"]),
|
||||
paper_bg = FALSE) {
|
||||
|
||||
theme_civilytics_slide(
|
||||
font_size = font_size,
|
||||
font_family = font_family,
|
||||
title_family = title_family,
|
||||
line_size = line_size,
|
||||
rel_small = rel_small,
|
||||
rel_tiny = rel_tiny,
|
||||
rel_large = rel_large,
|
||||
ink = ink,
|
||||
paper = paper,
|
||||
accent = accent,
|
||||
strip_color = strip_color,
|
||||
grid = "none",
|
||||
paper_bg = paper_bg
|
||||
) + .map_theme_extras()
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ pretty_count <- function(x) {
|
||||
|
||||
#' Unsuppress data using sampling
|
||||
#'
|
||||
#' @param x
|
||||
#' @param x a 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 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
|
||||
#'
|
||||
#' @return
|
||||
#' @return a numeric vector
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' grade_level_to_num(c("KG", "Pre-K", "12", "10", "09"))
|
||||
grade_level_to_num <- function(x) {
|
||||
# Cannot generate new levels if it is a factor so we coerce to character first
|
||||
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
|
||||
#'
|
||||
#' @return
|
||||
#' @return recoded race categories following NCES race codes
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' race_short_names(c("Black", "Hispanic Or Latino", "Two Or More Races"))
|
||||
race_short_names <- function(x) {
|
||||
x <- as.character(x)
|
||||
x[x %in% c("Black", "Black Or African American", "Black or African American",
|
||||
@@ -144,4 +146,322 @@ race_short_names <- function(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
|
||||
|
||||
<!-- badges: start -->
|
||||
<!-- badges: end -->
|
||||
|
||||
The goal of civilytics is to ...
|
||||
Brand themes, color palettes, and utility functions for [Civilytics
|
||||
Consulting](https://www.civilytics.com). The package provides a complete
|
||||
ggplot2 theme system drawn from the Civilytics design system — warm
|
||||
paper backgrounds, civic-navy ink, and editorial typography — along with
|
||||
10 curated color palettes, logo composition helpers, and data-wrangling
|
||||
utilities for public-sector analysis.
|
||||
|
||||
## Installation
|
||||
|
||||
You can install the released version of civilytics from [CRAN](https://CRAN.R-project.org) with:
|
||||
Install from the Civilytics Gitea server:
|
||||
|
||||
``` r
|
||||
install.packages("civilytics")
|
||||
# install.packages("remotes")
|
||||
remotes::install_git("https://gitea.civilytics.org/Civilytics/civilyticsR.git")
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
This is a basic example which shows you how to solve a common problem:
|
||||
## Quick start
|
||||
|
||||
``` r
|
||||
library(civilytics)
|
||||
## basic example code
|
||||
library(ggplot2)
|
||||
|
||||
ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||
geom_point(size = 2.5) +
|
||||
scale_color_civilytics() +
|
||||
labs(
|
||||
title = "Fuel economy by engine displacement",
|
||||
subtitle = "Highway MPG vs. engine size for 234 vehicles",
|
||||
caption = "Source: EPA fuel economy data (ggplot2::mpg)",
|
||||
x = "Engine displacement (litres)",
|
||||
y = "Highway MPG"
|
||||
) +
|
||||
theme_civilytics()
|
||||
```
|
||||
|
||||
<img src="man/figures/README-quickstart-1.png" alt="" width="100%" />
|
||||
|
||||
## Color palettes
|
||||
|
||||
The package ships 53 named brand colors in `civilytics_colors` and 10
|
||||
curated palettes in `civilytics_palettes`. Use `civilytics_palette()` to
|
||||
retrieve colors by name, or pass palettes directly to the ggplot2
|
||||
scales.
|
||||
|
||||
### Palette gallery
|
||||
|
||||
<img src="man/figures/README-palette-gallery-1.png" alt="" width="100%" />
|
||||
|
||||
### Using palettes
|
||||
|
||||
``` r
|
||||
# Discrete fill with the qualitative palette
|
||||
ggplot(mpg, aes(class, fill = class)) +
|
||||
geom_bar(show.legend = FALSE) +
|
||||
scale_fill_civilytics() +
|
||||
labs(title = "Vehicle counts by class", x = NULL, y = NULL) +
|
||||
theme_civilytics(grid = "y")
|
||||
```
|
||||
|
||||
<img src="man/figures/README-palette-usage-1.png" alt="" width="100%" />
|
||||
|
||||
``` r
|
||||
|
||||
# Continuous fill with a sequential palette
|
||||
ggplot(faithfuld, aes(waiting, eruptions, fill = density)) +
|
||||
geom_tile() +
|
||||
scale_fill_civilytics("seq_ember", discrete = FALSE) +
|
||||
labs(title = "Old Faithful eruption density") +
|
||||
theme_civilytics(grid = "none")
|
||||
```
|
||||
|
||||
<img src="man/figures/README-palette-usage-2.png" alt="" width="100%" />
|
||||
|
||||
## Themes
|
||||
|
||||
Three theme variants cover the most common output contexts. All share
|
||||
the same typographic structure and accept `grid` and `paper_bg`
|
||||
parameters.
|
||||
|
||||
### Editorial (default)
|
||||
|
||||
The default theme uses a warm paper background with horizontal gridlines
|
||||
— an editorial, Pew-style layout.
|
||||
|
||||
``` r
|
||||
base_plot <- ggplot(mpg, aes(displ, hwy)) +
|
||||
geom_point(aes(colour = factor(cyl)), size = 2) +
|
||||
scale_color_civilytics() +
|
||||
labs(
|
||||
title = "Engine size vs. highway fuel economy",
|
||||
subtitle = "Colored by number of cylinders",
|
||||
caption = "Source: ggplot2::mpg",
|
||||
colour = "Cylinders",
|
||||
x = "Displacement (L)", y = "Highway MPG"
|
||||
)
|
||||
|
||||
base_plot + theme_civilytics()
|
||||
```
|
||||
|
||||
<img src="man/figures/README-theme-editorial-1.png" alt="" width="100%" />
|
||||
|
||||
### Grid options
|
||||
|
||||
The `grid` parameter controls which major gridlines are drawn.
|
||||
|
||||
<img src="man/figures/README-theme-grids-1.png" alt="" width="100%" />
|
||||
|
||||
### Dark
|
||||
|
||||
Dark navy background with light text, suitable for presentations or
|
||||
dashboards on dark surfaces.
|
||||
|
||||
``` r
|
||||
base_plot + theme_civilytics_dark()
|
||||
```
|
||||
|
||||
<img src="man/figures/README-theme-dark-1.png" alt="" width="100%" />
|
||||
|
||||
### Slide
|
||||
|
||||
Transparent background and larger base font (18 pt), sized for Reveal.js
|
||||
slides or PowerPoint exports.
|
||||
|
||||
``` r
|
||||
base_plot + theme_civilytics_slide()
|
||||
```
|
||||
|
||||
<img src="man/figures/README-theme-slide-1.png" alt="" width="100%" />
|
||||
|
||||
### Facets
|
||||
|
||||
Facet strips use the `paper_2` tint, with the title font.
|
||||
|
||||
``` r
|
||||
ggplot(mpg, aes(displ, hwy)) +
|
||||
geom_point(colour = civilytics_colors["navy_600"], size = 1.5) +
|
||||
facet_wrap(~class, ncol = 4) +
|
||||
labs(
|
||||
title = "Highway MPG by vehicle class",
|
||||
x = "Displacement (L)", y = "Highway MPG"
|
||||
) +
|
||||
theme_civilytics(grid = "y")
|
||||
```
|
||||
|
||||
<img src="man/figures/README-theme-facets-1.png" alt="" width="100%" />
|
||||
|
||||
## Logo utilities
|
||||
|
||||
Add the Civilytics logo to any ggplot using the pipe-friendly
|
||||
`civilytics_logo()` or the lower-level `add_logo()` /
|
||||
`make_logo_grob()`. The logo is automatically right-aligned below the
|
||||
plot area. Wrap the ggplot chain in parentheses before piping — R’s `|>`
|
||||
binds tighter than `+`.
|
||||
|
||||
### Wordmark on a light plot
|
||||
|
||||
``` r
|
||||
p <- ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||
geom_point(size = 2) +
|
||||
scale_color_civilytics() +
|
||||
labs(
|
||||
title = "Fuel economy by engine displacement",
|
||||
caption = "Source: EPA fuel economy data (ggplot2::mpg)",
|
||||
x = "Displacement (L)", y = "Highway MPG"
|
||||
) +
|
||||
theme_civilytics()
|
||||
|
||||
grid::grid.draw(civilytics_logo(p))
|
||||
```
|
||||
|
||||
<img src="man/figures/README-logo-wordmark-1.png" alt="" width="100%" />
|
||||
|
||||
### Compact mark
|
||||
|
||||
Use `type = "mark"` for the compact C-pulse icon instead of the full
|
||||
wordmark.
|
||||
|
||||
``` r
|
||||
p2_logo <- ggplot(mpg, aes(displ, hwy, colour = factor(cyl))) +
|
||||
geom_point(size = 2) +
|
||||
scale_color_civilytics() +
|
||||
labs(
|
||||
title = "Engine size vs. highway fuel economy",
|
||||
colour = "Cylinders",
|
||||
x = "Displacement (L)", y = "Highway MPG"
|
||||
) +
|
||||
theme_civilytics()
|
||||
|
||||
grid::grid.draw(civilytics_logo(p2_logo, type = "mark"))
|
||||
```
|
||||
|
||||
<img src="man/figures/README-logo-mark-1.png" alt="" width="100%" />
|
||||
|
||||
### Multi-plot layout with logo
|
||||
|
||||
Use `add_logo_ga()` to attach a single logo below a row of plots.
|
||||
|
||||
``` r
|
||||
p1 <- ggplot(mpg, aes(class, fill = class)) +
|
||||
geom_bar(show.legend = FALSE) +
|
||||
scale_fill_civilytics() +
|
||||
labs(title = "Vehicle counts", x = NULL, y = NULL) +
|
||||
theme_civilytics(grid = "y")
|
||||
|
||||
p2 <- ggplot(mpg, aes(displ, hwy)) +
|
||||
geom_point(colour = civilytics_colors["navy_600"], size = 1.5) +
|
||||
labs(title = "Displacement vs. MPG", x = "Displacement (L)", y = "Highway MPG") +
|
||||
theme_civilytics(grid = "y")
|
||||
|
||||
logo <- make_logo_grob()
|
||||
grid::grid.draw(add_logo_ga(list(p1, p2), logo))
|
||||
```
|
||||
|
||||
<img src="man/figures/README-logo-multi-1.png" alt="" width="100%" />
|
||||
|
||||
## Other utilities
|
||||
|
||||
The package also includes helpers for public-sector data analysis:
|
||||
|
||||
| Function | Purpose |
|
||||
|:---|:---|
|
||||
| `pretty_count()` / `pretty_per()` | Format numbers and percentages |
|
||||
| `grade_level_to_num()` | Convert grade labels (KG, 01–12) to numeric |
|
||||
| `race_short_names()` | Standardize NCES race/ethnicity categories |
|
||||
| `get_fips()` / `get_stabbr()` | State FIPS code lookups |
|
||||
| `clopper_pearson()` / `agresti_coull_interval()` | Proportion confidence intervals |
|
||||
| `match_test()` / `trunc_match()` | Fuzzy join diagnostics |
|
||||
| `perturb_count()` / `random_round()` | Privacy-preserving data perturbation |
|
||||
|
||||
## Maintaining brand assets
|
||||
|
||||
Logo and brand mark files live in `inst/img/`. The package ships both
|
||||
PNG (for ggplot2 raster composition) and SVG (for Quarto/HTML output)
|
||||
variants:
|
||||
|
||||
| File | Format | Used by |
|
||||
|:---|:---|:---|
|
||||
| `civilytics-wordmark.png` / `.svg` | Full “Civilytics” lockup | `make_logo_grob("wordmark", "light")`, Quarto templates |
|
||||
| `civilytics-wordmark-reverse.png` / `.svg` | Light-on-dark wordmark | `make_logo_grob("wordmark", "dark")`, dark slides |
|
||||
| `civilytics-mark.png` / `.svg` | Compact C-pulse icon | `make_logo_grob("mark", "light")` |
|
||||
| `civilytics-mark-reverse.svg` | Light-on-dark mark | `make_logo_grob("mark", "dark")` |
|
||||
| `civilytics-pulse.svg` | Standalone waveform glyph | Quarto slide footer chrome |
|
||||
|
||||
To update the logos, replace the files in `inst/img/` with new versions
|
||||
using the same filenames. The PNG files must be raster images (the
|
||||
ggplot2 logo functions read them via `png::readPNG()`). SVG files are
|
||||
passed through as-is by Quarto and HTML templates.
|
||||
|
||||
After replacing files, re-render the README gallery to update the
|
||||
screenshots:
|
||||
|
||||
``` r
|
||||
devtools::load_all()
|
||||
rmarkdown::render("README.Rmd")
|
||||
```
|
||||
|
||||
|
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,80 @@
|
||||
---
|
||||
title: "Who pays when rent outpaces wages?"
|
||||
subtitle: "A 12-county analysis of cost-burdened renter households, 2019–2024."
|
||||
author:
|
||||
- name: "Civilytics Research"
|
||||
affiliation: "Civilytics Consulting"
|
||||
date: "2026-04-15"
|
||||
abstract: |
|
||||
We combine ACS 5-year microdata with HUD Fair Market Rent estimates
|
||||
to track changes in rent burden across 12 metropolitan counties.
|
||||
Between 2019 and 2024, the share of renter households spending more
|
||||
than 30% of income on housing rose by 6.4 percentage points; the
|
||||
increase was concentrated in counties where median wages stagnated.
|
||||
categories: [Housing, Affordability, ACS]
|
||||
format:
|
||||
html:
|
||||
theme: ../theme/civilytics.scss
|
||||
css: ../theme/extras.css
|
||||
toc: true
|
||||
toc-location: right
|
||||
typst:
|
||||
template: ../typst/civilytics-typst.typ
|
||||
pdf:
|
||||
include-in-header: ../latex/civilytics.tex
|
||||
include-before-body: ../latex/civilytics-title.tex
|
||||
execute:
|
||||
echo: false
|
||||
warning: false
|
||||
---
|
||||
|
||||
## Findings at a glance
|
||||
|
||||
::: {.stat-callout}
|
||||
::: {}
|
||||
[6.4 pp]{.stat-num}
|
||||
[Increase in rent-burdened share, 2019→2024]{.stat-label .accent}
|
||||
:::
|
||||
::: {}
|
||||
[1 in 2]{.stat-num}
|
||||
[Renters in the studied counties now cost-burdened]{.stat-label}
|
||||
:::
|
||||
::: {}
|
||||
[$412]{.stat-num}
|
||||
[Median monthly rent gap, lowest-wage quintile]{.stat-label}
|
||||
:::
|
||||
:::
|
||||
|
||||
[Methodology note]{.eyebrow}
|
||||
|
||||
## Background
|
||||
|
||||
Rent has outpaced wages in every county we studied. In some, the gap is
|
||||
modest and recent; in others, it has been widening for a decade. This
|
||||
report disaggregates the trend along three lines: county, income
|
||||
quintile, and household composition.
|
||||
|
||||
```{r}
|
||||
#| label: fig-trend
|
||||
#| fig-cap: "Cost-burdened renter share, 12 metro counties, 2019–2024."
|
||||
#| fig-width: 7
|
||||
#| fig-height: 4.2
|
||||
library(ggplot2); library(civilytics)
|
||||
ggplot(economics, aes(date, unemploy / pop)) +
|
||||
geom_line(color = "#C25311", linewidth = 1) +
|
||||
labs(title = "Cost-burdened renter share, 2019–2024",
|
||||
subtitle = "Share spending more than 30% of household income on rent",
|
||||
x = NULL, y = NULL,
|
||||
caption = "Source: ACS 5-yr microdata; Civilytics analysis.") +
|
||||
scale_y_continuous(labels = scales::percent_format()) +
|
||||
theme_civilytics()
|
||||
```
|
||||
|
||||
> Across all 12 counties, the steepest increases came in places where
|
||||
> median wages were already in the bottom quartile of the metro region.
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. Targeted rental assistance keyed to local wage trajectories.
|
||||
2. Quarterly publication of rent-burden indicators by county.
|
||||
3. Coordinated reporting between HUD and county human-services offices.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "Quarterly Analysis"
|
||||
subtitle: "A brief look at the numbers."
|
||||
author: "Civilytics Consulting"
|
||||
date: today
|
||||
format: civilytics-reveal-revealjs
|
||||
---
|
||||
|
||||
## Findings at a glance
|
||||
|
||||
::: {.columns}
|
||||
::: {.column width="50%"}
|
||||
|
||||
[Key metric]{.stat-number}
|
||||
|
||||
- Point one
|
||||
- Point two
|
||||
- Point three
|
||||
|
||||
:::
|
||||
::: {.column width="50%"}
|
||||
|
||||
```{r}
|
||||
#| fig-width: 6
|
||||
#| fig-height: 4
|
||||
library(ggplot2)
|
||||
library(civilytics)
|
||||
|
||||
ggplot(mpg, aes(displ, hwy, colour = class)) +
|
||||
geom_point(size = 2.5) +
|
||||
scale_color_civilytics() +
|
||||
theme_civilytics_slide() +
|
||||
labs(title = "Engine Size vs Fuel Economy",
|
||||
x = "Displacement (L)", y = "Highway MPG")
|
||||
```
|
||||
|
||||
:::
|
||||
:::
|
||||
|
||||
## Section divider {.section}
|
||||
|
||||
Big idea goes here.
|
||||
|
||||
## Data table
|
||||
|
||||
| County | Burden (%) | Change |
|
||||
|:-------------|:----------:|-------:|
|
||||
| Dane | 38.2 | +4.1 |
|
||||
| Milwaukee | 52.7 | +7.3 |
|
||||
| Hennepin | 41.5 | +3.9 |
|
||||
|
||||
: Cost-burdened renter share, selected counties.
|
||||
|
||||
## A blockquote slide {.pullquote}
|
||||
|
||||
> Rent has outpaced wages in every county we studied.
|
||||
|
||||
Civilytics Research, 2026
|
||||
|
||||
## Thank you {.thank-you}
|
||||
|
||||
Questions?
|
||||
|
||||
- jared@civilytics.com
|
||||
- civilytics.consulting
|
||||
@@ -0,0 +1,56 @@
|
||||
title: Civilytics Reveal
|
||||
author: Civilytics Consulting
|
||||
version: 1.0.0
|
||||
quarto-required: ">=1.4.0"
|
||||
contributes:
|
||||
formats:
|
||||
revealjs:
|
||||
# ---- Theme ----
|
||||
theme: [default, civilytics.scss]
|
||||
|
||||
# ---- Slide geometry ----
|
||||
width: 1280
|
||||
height: 720
|
||||
margin: 0
|
||||
min-scale: 0.2
|
||||
max-scale: 2.0
|
||||
center: false
|
||||
|
||||
# ---- Navigation / chrome ----
|
||||
controls: true
|
||||
controls-layout: edges
|
||||
progress: true
|
||||
slide-number: "c/t"
|
||||
hash-type: number
|
||||
history: true
|
||||
navigation-mode: linear
|
||||
transition: fade
|
||||
transition-speed: fast
|
||||
background-transition: fade
|
||||
|
||||
# ---- Code ----
|
||||
highlight-style: github
|
||||
code-line-numbers: true
|
||||
code-copy: true
|
||||
code-overflow: wrap
|
||||
|
||||
# ---- Math ----
|
||||
html-math-method: mathjax
|
||||
|
||||
# ---- Tables ----
|
||||
tbl-cap-location: top
|
||||
|
||||
# ---- Links / fragments ----
|
||||
link-external-newwindow: true
|
||||
incremental: false
|
||||
|
||||
# ---- Brand chrome (injected by civilytics.js) ----
|
||||
include-in-header:
|
||||
- file: civilytics-head.html
|
||||
include-after-body:
|
||||
- file: civilytics-after.html
|
||||
|
||||
# ---- Footer / logo ----
|
||||
# Override in the document front-matter if desired:
|
||||
# footer: "Civilytics Consulting · 2026"
|
||||
# logo: _extensions/civilytics-reveal/assets/civilytics-mark.svg
|
||||
@@ -0,0 +1,80 @@
|
||||
<!-- Civilytics Reveal — brand chrome injector -->
|
||||
<script>
|
||||
(function () {
|
||||
// The Civilytics wordmark is the bold serif "Civilytics" lockup with the pulse
|
||||
// waveform riding inline at the end. It ships as a baked SVG in two variants:
|
||||
// ink-on-paper (default) and paper-on-ink (reverse) for dark backgrounds.
|
||||
// We render it as an <img> so the official artwork is preserved exactly —
|
||||
// the file picks up its color from the SVG itself, not from CSS currentColor.
|
||||
var ASSET_BASE = (function () {
|
||||
// Find the URL this script's <link rel="stylesheet" href="..civilytics.css..">
|
||||
// was loaded from, so we can resolve sibling assets the same way.
|
||||
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var href = links[i].getAttribute('href') || '';
|
||||
var m = href.match(/^(.*\/)civilytics\.css(\?|$)/);
|
||||
if (m) return m[1] + 'assets/';
|
||||
}
|
||||
// Fallback: assume Quarto-style sibling layout
|
||||
return '_extensions/civilytics-reveal/assets/';
|
||||
})();
|
||||
|
||||
function makeWordmarkImg(reverse) {
|
||||
var img = document.createElement('img');
|
||||
img.className = 'civilytics-wordmark';
|
||||
img.alt = 'Civilytics';
|
||||
img.src = ASSET_BASE + (reverse ? 'civilytics-wordmark-reverse.svg' : 'civilytics-wordmark.svg');
|
||||
return img;
|
||||
}
|
||||
|
||||
function injectWordmark(slide, reverse) {
|
||||
if (!slide) return;
|
||||
if (slide.querySelector(':scope > .civilytics-wordmark')) return;
|
||||
slide.insertBefore(makeWordmarkImg(reverse), slide.firstChild);
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!window.Reveal) { setTimeout(init, 50); return; }
|
||||
var root = document.querySelector('.reveal');
|
||||
if (!root) return;
|
||||
|
||||
// Title slide — paper background, ink wordmark
|
||||
document.querySelectorAll(
|
||||
'.slides #title-slide, .slides section.title-slide'
|
||||
).forEach(function (s) { injectWordmark(s, false); });
|
||||
|
||||
// Thank-you slide — ink background, reversed wordmark
|
||||
document.querySelectorAll('.slides section.thank-you').forEach(function (s) {
|
||||
injectWordmark(s, true);
|
||||
});
|
||||
|
||||
// Brand footer (pulse glyph + "Civilytics") bottom-left on body slides.
|
||||
// Uses inline SVG so it inherits the slide's text color via currentColor.
|
||||
if (!root.querySelector('.civilytics-footer')) {
|
||||
var footer = document.createElement('div');
|
||||
footer.className = 'civilytics-footer';
|
||||
footer.setAttribute('aria-hidden', 'true');
|
||||
footer.innerHTML =
|
||||
'<svg viewBox="0 0 30 14" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter">' +
|
||||
'<path d="M0 8 L5 8 L8 3 L12 13 L16 1 L20 11 L23 8 L30 8"/>' +
|
||||
'</svg>' +
|
||||
'<span>Civilytics</span>';
|
||||
root.appendChild(footer);
|
||||
}
|
||||
|
||||
function updateChrome() {
|
||||
var current = Reveal.getCurrentSlide();
|
||||
if (!current) return;
|
||||
var hide = current.matches('.title-slide, #title-slide, .section-divider, .section, .thank-you, .full-bleed, .no-chrome');
|
||||
document.body.classList.toggle('civilytics-hide-chrome', !!hide);
|
||||
}
|
||||
Reveal.on('ready', updateChrome);
|
||||
Reveal.on('slidechanged', updateChrome);
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
<!-- Civilytics Reveal — fonts preconnect (theme CSS also @imports, this just warms the connection) -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
@@ -0,0 +1,41 @@
|
||||
% Civilytics — title-page partial.
|
||||
% Replaces Quarto's default \maketitle. Uses values from YAML
|
||||
% (\thetitle, \theauthor, \thedate) plus an \ifabstract block.
|
||||
|
||||
\begin{titlepage}
|
||||
\pagecolor{paper}
|
||||
\color{ink}
|
||||
\vspace*{0.5in}
|
||||
{\sffamily\bfseries\scriptsize\color{ember}\MakeUppercase{— Civilytics Research}\par}
|
||||
\vspace{12pt}
|
||||
{\displayfont\fontsize{32pt}{34pt}\selectfont\bfseries\color{ink}\thetitle\par}
|
||||
\vspace{8pt}
|
||||
\ifx\thesubtitle\@empty\else
|
||||
{\rmfamily\itshape\fontsize{14pt}{18pt}\selectfont\color{ink2}\thesubtitle\par}
|
||||
\fi
|
||||
\vspace{18pt}
|
||||
\noindent\rule{\linewidth}{2pt}\par
|
||||
\vspace{1pt}
|
||||
\noindent{\color{rule}\rule{\linewidth}{0.4pt}}\par
|
||||
\vspace{8pt}
|
||||
\noindent
|
||||
\begin{minipage}[t]{0.5\linewidth}
|
||||
{\sffamily\scriptsize\color{ink3}\MakeUppercase{Authors}\par}
|
||||
\vspace{2pt}
|
||||
{\sffamily\small\color{ink}\theauthor\par}
|
||||
\end{minipage}\hfill
|
||||
\begin{minipage}[t]{0.4\linewidth}\raggedleft
|
||||
{\sffamily\scriptsize\color{ink3}\MakeUppercase{Published}\par}
|
||||
\vspace{2pt}
|
||||
{\sffamily\small\color{ink}\thedate\par}
|
||||
\end{minipage}\par
|
||||
\vspace{1.5cm}
|
||||
|
||||
% Pulse mark, in ember
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[overlay, remember picture]
|
||||
\end{tikzpicture}
|
||||
{\color{ember}\rule{40pt}{2pt}}
|
||||
\end{center}
|
||||
\end{titlepage}
|
||||
\clearpage
|
||||
@@ -0,0 +1,112 @@
|
||||
% =============================================================
|
||||
% Civilytics — LaTeX preamble for Quarto PDF
|
||||
% Usage in YAML:
|
||||
% format:
|
||||
% pdf:
|
||||
% include-in-header: quarto/latex/civilytics.tex
|
||||
% include-before-body: quarto/latex/civilytics-title.tex
|
||||
% Requires: xelatex or lualatex (for system fonts).
|
||||
% =============================================================
|
||||
|
||||
\usepackage{xcolor}
|
||||
\usepackage{fontspec}
|
||||
\usepackage{titlesec}
|
||||
\usepackage{titling}
|
||||
\usepackage{fancyhdr}
|
||||
\usepackage{enumitem}
|
||||
\usepackage{booktabs}
|
||||
\usepackage{caption}
|
||||
\usepackage[hidelinks]{hyperref}
|
||||
\usepackage{microtype}
|
||||
\usepackage{tcolorbox}
|
||||
\tcbuselibrary{skins,breakable}
|
||||
|
||||
% --- Civilytics palette ---
|
||||
\definecolor{paper}{HTML}{FAF7F2}
|
||||
\definecolor{paper2}{HTML}{F2EDE4}
|
||||
\definecolor{ink}{HTML}{0E1A2B}
|
||||
\definecolor{ink2}{HTML}{2B3A52}
|
||||
\definecolor{ink3}{HTML}{5A6A82}
|
||||
\definecolor{rule}{HTML}{D6CEBD}
|
||||
\definecolor{ruleStrong}{HTML}{B8AE97}
|
||||
\definecolor{navy}{HTML}{22406A}
|
||||
\definecolor{ember}{HTML}{C25311}
|
||||
\definecolor{emberDark}{HTML}{923D00}
|
||||
\definecolor{teal}{HTML}{1F6F70}
|
||||
\definecolor{plum}{HTML}{6B3A5E}
|
||||
|
||||
% --- Page color ---
|
||||
\pagecolor{paper}
|
||||
\color{ink}
|
||||
|
||||
% --- Fonts (require local install or fontspec lookup) ---
|
||||
\setmainfont{Source Serif 4}[
|
||||
UprightFont = *,
|
||||
ItalicFont = * Italic,
|
||||
BoldFont = * SemiBold,
|
||||
BoldItalicFont = * SemiBold Italic,
|
||||
Ligatures = TeX,
|
||||
]
|
||||
\setsansfont{Inter}[Ligatures = TeX]
|
||||
\setmonofont{JetBrains Mono}[Scale = 0.92]
|
||||
\newfontfamily\displayfont{Libre Franklin}[Ligatures = TeX]
|
||||
|
||||
% --- Hyperlinks ---
|
||||
\hypersetup{
|
||||
colorlinks = true,
|
||||
linkcolor = navy,
|
||||
urlcolor = navy,
|
||||
citecolor = navy,
|
||||
filecolor = navy,
|
||||
}
|
||||
|
||||
% --- Section headings ---
|
||||
\titleformat{\section}[block]
|
||||
{\bfseries\Large\displayfont\color{ink}}
|
||||
{}{0pt}
|
||||
{\titlerule[0.4pt]\vspace{4pt}}[\vspace{-6pt}]
|
||||
\titleformat{\subsection}[block]
|
||||
{\bfseries\large\displayfont\color{ink}}{}{0pt}{}
|
||||
\titleformat{\subsubsection}[block]
|
||||
{\bfseries\normalsize\displayfont\color{ink}}{}{0pt}{}
|
||||
|
||||
% --- Page header / footer ---
|
||||
\pagestyle{fancy}
|
||||
\fancyhf{}
|
||||
\renewcommand{\headrulewidth}{0pt}
|
||||
\renewcommand{\footrulewidth}{0pt}
|
||||
\fancyhead[L]{\sffamily\scriptsize\color{ink3}\MakeUppercase{Civilytics Consulting}}
|
||||
\fancyhead[R]{\sffamily\scriptsize\color{ink3}\thetitle}
|
||||
\fancyfoot[L]{\sffamily\scriptsize\color{ink3}civilytics.consulting}
|
||||
\fancyfoot[C]{\sffamily\scriptsize\color{ink3}\thepage}
|
||||
\fancyfoot[R]{\sffamily\scriptsize\color{ink3}\textcopyright\ 2026}
|
||||
|
||||
% --- Captions ---
|
||||
\captionsetup{
|
||||
font={sf,small,color=ink3},
|
||||
labelfont={sf,bf,color=ink3},
|
||||
labelsep=period,
|
||||
justification=raggedright,
|
||||
singlelinecheck=false,
|
||||
}
|
||||
|
||||
% --- Pullquote ---
|
||||
\newtcolorbox{pullquote}{
|
||||
enhanced, breakable, frame hidden, colback=paper,
|
||||
borderline west = {2pt}{0pt}{ember},
|
||||
left=14pt, right=4pt, top=4pt, bottom=4pt,
|
||||
fontupper={\itshape\rmfamily\large\color{ink}},
|
||||
}
|
||||
|
||||
% --- Code blocks (Pandoc + listings or minted both inherit colors below) ---
|
||||
\definecolor{codebg}{HTML}{0E1A2B}
|
||||
\definecolor{codefg}{HTML}{FAF7F2}
|
||||
|
||||
% --- Lists tighter ---
|
||||
\setlist{itemsep=2pt, topsep=4pt}
|
||||
|
||||
% --- Tabular numerals ---
|
||||
\newcommand{\tnum}[1]{{\addfontfeature{Numbers={Tabular,Lining}}#1}}
|
||||
|
||||
% --- Ember accent rule above section starts ---
|
||||
\newcommand{\emberrule}{{\color{ember}\rule{60pt}{3pt}}\par\vspace{4pt}}
|
||||
@@ -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,244 @@
|
||||
// =============================================================
|
||||
// Civilytics — Typst template for Quarto PDF
|
||||
// Usage in YAML:
|
||||
// format:
|
||||
// typst:
|
||||
// template: quarto/typst/civilytics-typst.typ
|
||||
// =============================================================
|
||||
|
||||
#let paper-bg = rgb("#FAF7F2")
|
||||
#let ink = rgb("#0E1A2B")
|
||||
#let ink-2 = rgb("#2B3A52")
|
||||
#let ink-3 = rgb("#5A6A82")
|
||||
#let rule = rgb("#D6CEBD")
|
||||
#let rule-strong = rgb("#B8AE97")
|
||||
#let navy = rgb("#22406A")
|
||||
#let ember = rgb("#C25311")
|
||||
#let ember-dark = rgb("#923D00")
|
||||
#let plum = rgb("#6B3A5E")
|
||||
#let teal = rgb("#1F6F70")
|
||||
|
||||
#let serif-stack = ("Source Serif 4", "Source Serif Pro", "Georgia", "Times New Roman")
|
||||
#let sans-stack = ("Inter", "Helvetica Neue", "Arial")
|
||||
#let display-stack = ("Libre Franklin", "Inter", "Helvetica Neue")
|
||||
#let mono-stack = ("JetBrains Mono", "Menlo", "Consolas")
|
||||
|
||||
#let civilytics(
|
||||
title: none,
|
||||
subtitle: none,
|
||||
authors: (),
|
||||
date: none,
|
||||
abstract: none,
|
||||
toc: true,
|
||||
doc,
|
||||
) = {
|
||||
// Page setup
|
||||
set page(
|
||||
paper: "us-letter",
|
||||
margin: (top: 1in, bottom: 1in, left: 1.1in, right: 1.1in),
|
||||
fill: paper-bg,
|
||||
header: context {
|
||||
if counter(page).get().first() > 1 {
|
||||
set text(font: sans-stack, size: 8pt, fill: ink-3, tracking: 0.06em)
|
||||
upper[Civilytics Consulting]
|
||||
h(1fr)
|
||||
upper(if title != none { title } else { "" })
|
||||
v(2pt)
|
||||
line(length: 100%, stroke: 0.4pt + rule)
|
||||
}
|
||||
},
|
||||
footer: context {
|
||||
set text(font: sans-stack, size: 8pt, fill: ink-3)
|
||||
line(length: 100%, stroke: 0.4pt + rule)
|
||||
v(4pt)
|
||||
grid(
|
||||
columns: (1fr, auto, 1fr),
|
||||
align: (left, center, right),
|
||||
[civilytics.consulting],
|
||||
counter(page).display("1 / 1", both: true),
|
||||
[© 2026]
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// Body text
|
||||
set text(font: serif-stack, size: 10.5pt, fill: ink, lang: "en")
|
||||
set par(justify: false, leading: 0.6em, first-line-indent: 0pt)
|
||||
|
||||
// Headings
|
||||
show heading.where(level: 1): it => {
|
||||
v(1.4em)
|
||||
block[
|
||||
#set text(font: display-stack, weight: 900, size: 22pt, fill: ink, tracking: -0.025em)
|
||||
#it.body
|
||||
]
|
||||
v(0.2em)
|
||||
line(length: 60pt, stroke: 3pt + ember)
|
||||
v(0.6em)
|
||||
}
|
||||
show heading.where(level: 2): it => {
|
||||
v(1.1em)
|
||||
line(length: 100%, stroke: 0.5pt + rule)
|
||||
v(0.5em)
|
||||
block[
|
||||
#set text(font: display-stack, weight: 800, size: 16pt, fill: ink, tracking: -0.02em)
|
||||
#it.body
|
||||
]
|
||||
v(0.3em)
|
||||
}
|
||||
show heading.where(level: 3): it => {
|
||||
v(0.8em)
|
||||
block[
|
||||
#set text(font: display-stack, weight: 700, size: 13pt, fill: ink, tracking: -0.015em)
|
||||
#it.body
|
||||
]
|
||||
v(0.2em)
|
||||
}
|
||||
show heading.where(level: 4): it => block[
|
||||
#set text(font: sans-stack, weight: 600, size: 11pt, fill: ink-2)
|
||||
#it.body
|
||||
]
|
||||
|
||||
// Links
|
||||
show link: it => text(fill: navy, underline(it))
|
||||
|
||||
// Inline code
|
||||
show raw.where(block: false): it => box(
|
||||
fill: rgb("#F2EDE4"),
|
||||
inset: (x: 3pt, y: 1pt),
|
||||
outset: (y: 2pt),
|
||||
radius: 1pt,
|
||||
text(font: mono-stack, size: 0.92em, fill: ember-dark)[#it]
|
||||
)
|
||||
|
||||
// Code blocks
|
||||
show raw.where(block: true): it => block(
|
||||
fill: ink,
|
||||
width: 100%,
|
||||
inset: 12pt,
|
||||
radius: 4pt,
|
||||
)[
|
||||
#set text(font: mono-stack, size: 8.5pt, fill: paper-bg)
|
||||
#it
|
||||
]
|
||||
|
||||
// Block quote
|
||||
show quote.where(block: true): it => block(
|
||||
stroke: (left: 2pt + ember),
|
||||
inset: (left: 14pt, top: 4pt, bottom: 4pt),
|
||||
spacing: 1.2em,
|
||||
)[
|
||||
#set text(font: serif-stack, size: 13pt, style: "italic", fill: ink)
|
||||
#it.body
|
||||
]
|
||||
|
||||
// Figures
|
||||
show figure: it => block(spacing: 1.4em)[
|
||||
#it.body
|
||||
#v(4pt)
|
||||
#line(length: 100%, stroke: 0.4pt + rule)
|
||||
#v(2pt)
|
||||
#set text(font: sans-stack, size: 8.5pt, fill: ink-3)
|
||||
#it.caption
|
||||
]
|
||||
|
||||
// Tables
|
||||
set table(
|
||||
stroke: (x, y) => (
|
||||
top: if y == 0 { 1.5pt + ink } else if y == 1 { 0.6pt + rule-strong } else { 0pt },
|
||||
bottom: if y == 0 { 0pt } else { 0.4pt + rule },
|
||||
),
|
||||
inset: (x: 8pt, y: 6pt),
|
||||
)
|
||||
show table.cell.where(y: 0): set text(
|
||||
font: sans-stack, size: 8pt, weight: 600, fill: ink-3, tracking: 0.06em
|
||||
)
|
||||
|
||||
// ----- Title block -----
|
||||
if title != none {
|
||||
block[
|
||||
#set text(font: sans-stack, size: 8pt, weight: 600, fill: ember, tracking: 0.1em)
|
||||
#upper[— Civilytics Research]
|
||||
]
|
||||
v(8pt)
|
||||
block[
|
||||
#set text(font: display-stack, weight: 900, size: 32pt, fill: ink, tracking: -0.035em)
|
||||
#set par(leading: 0.4em)
|
||||
#title
|
||||
]
|
||||
if subtitle != none {
|
||||
v(8pt)
|
||||
block[
|
||||
#set text(font: serif-stack, style: "italic", size: 14pt, fill: ink-2)
|
||||
#set par(leading: 0.55em)
|
||||
#subtitle
|
||||
]
|
||||
}
|
||||
v(18pt)
|
||||
line(length: 100%, stroke: 3pt + ink)
|
||||
v(2pt)
|
||||
line(length: 100%, stroke: 0.4pt + rule)
|
||||
v(8pt)
|
||||
|
||||
// Authors + date row
|
||||
grid(
|
||||
columns: (1fr, 1fr),
|
||||
align: (left, right),
|
||||
[
|
||||
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
|
||||
#upper[Authors] \
|
||||
#set text(font: sans-stack, size: 10pt, fill: ink, weight: 500, tracking: 0em)
|
||||
#if authors.len() > 0 {
|
||||
authors.map(a => if type(a) == str { a } else { a.name }).join(", ")
|
||||
}
|
||||
],
|
||||
[
|
||||
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
|
||||
#upper[Published] \
|
||||
#set text(font: sans-stack, size: 10pt, fill: ink, weight: 500, tracking: 0em)
|
||||
#if date != none { date }
|
||||
]
|
||||
)
|
||||
v(24pt)
|
||||
|
||||
if abstract != none {
|
||||
block(
|
||||
stroke: (left: 2pt + ember),
|
||||
inset: (left: 14pt),
|
||||
)[
|
||||
#set text(font: serif-stack, style: "italic", size: 12pt, fill: ink)
|
||||
#set par(leading: 0.55em)
|
||||
#abstract
|
||||
]
|
||||
v(20pt)
|
||||
}
|
||||
|
||||
if toc {
|
||||
block[
|
||||
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
|
||||
#upper[Contents]
|
||||
]
|
||||
v(4pt)
|
||||
line(length: 100%, stroke: 0.4pt + rule)
|
||||
v(8pt)
|
||||
outline(title: none, indent: auto, depth: 3)
|
||||
v(20pt)
|
||||
pagebreak()
|
||||
}
|
||||
}
|
||||
|
||||
doc
|
||||
}
|
||||
|
||||
// Quarto entry point
|
||||
#show: doc => civilytics(
|
||||
title: $title$,
|
||||
$if(subtitle)$subtitle: $subtitle$,$endif$
|
||||
$if(by-author)$authors: ($for(by-author)$"$it.name.literal$",$endfor$),$endif$
|
||||
$if(date)$date: $date$,$endif$
|
||||
$if(abstract)$abstract: [$abstract$],$endif$
|
||||
toc: $if(toc)$true$else$false$endif$,
|
||||
doc
|
||||
)
|
||||
|
||||
$body$
|
||||
@@ -4,7 +4,13 @@
|
||||
\alias{add_logo}
|
||||
\title{Add a logo to a ggplot2 object}
|
||||
\usage{
|
||||
add_logo(plot, logo, margin_param = NULL)
|
||||
add_logo(
|
||||
plot,
|
||||
logo,
|
||||
margin_param = NULL,
|
||||
font_scale = 1.1,
|
||||
position = c("bottom-right", "bottom-left", "top-right", "top-left")
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
\item{plot}{a ggplot2 grob}
|
||||
@@ -12,6 +18,18 @@ add_logo(plot, logo, margin_param = NULL)
|
||||
\item{logo}{a logo grob created by make_logo_grob()}
|
||||
|
||||
\item{margin_param}{a numeric specifying what margin to add or subtract to align the logo}
|
||||
|
||||
\item{font_scale}{Numeric. Multiplicative scaling factor applied to text
|
||||
sizes before composing the plot with the logo. Default `1.1` inflates
|
||||
text by ~10 \% to compensate for the viewport shrinkage caused by
|
||||
[gridExtra::arrangeGrob()]. Set to `1` to disable.}
|
||||
|
||||
\item{position}{Character. Corner placement for the logo: `"bottom-right"`
|
||||
(default), `"bottom-left"`, `"top-right"`, or `"top-left"`. Controls
|
||||
whether the logo is placed above or below the plot.}
|
||||
}
|
||||
\value{
|
||||
a grob with a logo attached to it ready to plot
|
||||
}
|
||||
\description{
|
||||
Add a logo to a ggplot2 object
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
% Please edit documentation in R/logo.R
|
||||
\name{add_logo_ga}
|
||||
\alias{add_logo_ga}
|
||||
\title{Add a logo to a ggplot2 object}
|
||||
\title{Add a logo to multiple ggplot2 objects}
|
||||
\usage{
|
||||
add_logo_ga(plot_list, logo, nrow = 1, widths = NULL, margin_param = NULL)
|
||||
add_logo_ga(
|
||||
plot_list,
|
||||
logo,
|
||||
nrow = 1,
|
||||
widths = NULL,
|
||||
margin_param = NULL,
|
||||
font_scale = 1.1
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
\item{plot_list}{a list containing ggplot2 objects}
|
||||
@@ -16,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{margin_param}{a number giving the adjustment up or down to help manually align logo and captions}
|
||||
|
||||
\item{font_scale}{Numeric. Multiplicative scaling factor applied to text
|
||||
sizes before composing. Default `1.1`. See [add_logo()] for details.}
|
||||
}
|
||||
\value{
|
||||
a grid object
|
||||
}
|
||||
\description{
|
||||
Add a logo to a ggplot2 object
|
||||
Add a logo to multiple ggplot2 objects
|
||||
}
|
||||
\note{
|
||||
The resulting object needs to be drawn to the screen using grid.draw()
|
||||
}
|
||||
\examples{
|
||||
\dontrun{
|
||||
library(ggplot2); library(grid)
|
||||
tmp_plot <- ggplot(mtcars) + aes(x = hp, y = disp) + geom_point() + theme_civilytics()
|
||||
tmp_logo <- make_logo_grob()
|
||||
@@ -31,3 +45,4 @@ plot_and_logo <- add_logo(tmp_plot, tmp_logo)
|
||||
grid.draw(plot_and_logo)
|
||||
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,23 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/fonts.R
|
||||
\name{civilytics_load_fonts}
|
||||
\alias{civilytics_load_fonts}
|
||||
\title{Load Civilytics brand fonts}
|
||||
\usage{
|
||||
civilytics_load_fonts()
|
||||
}
|
||||
\value{
|
||||
Invisibly returns `NULL`.
|
||||
}
|
||||
\description{
|
||||
Downloads Inter and Libre Franklin from Google Fonts via
|
||||
[sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so
|
||||
that all graphics devices render text with those fonts. This is called
|
||||
automatically when the package loads; use this function to retry if the
|
||||
initial load failed (e.g., the machine was offline at load time).
|
||||
}
|
||||
\examples{
|
||||
\dontrun{
|
||||
civilytics_load_fonts()
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
\item{x}{a character vector}
|
||||
}
|
||||
\value{
|
||||
An integer counting the number of "." occurences in a vector
|
||||
}
|
||||
\description{
|
||||
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{
|
||||
\item{filename}{a character with file path to a png file}
|
||||
}
|
||||
\value{
|
||||
a plotted rasteGrob of a png image
|
||||
}
|
||||
\description{
|
||||
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{
|
||||
\item{x}{character description of grade levels from NCES style data}
|
||||
}
|
||||
\value{
|
||||
a numeric vector
|
||||
}
|
||||
\description{
|
||||
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
|
||||
}
|
||||
\examples{
|
||||
p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
|
||||
p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
|
||||
has_caption(p1) # FALSE
|
||||
}
|
||||
|
||||
@@ -4,16 +4,35 @@
|
||||
\alias{make_logo_grob}
|
||||
\title{Get a Civilytics Logo grob}
|
||||
\usage{
|
||||
make_logo_grob()
|
||||
make_logo_grob(
|
||||
type = c("wordmark", "mark"),
|
||||
variant = c("light", "dark"),
|
||||
position = c("bottom-right", "bottom-left", "top-right", "top-left")
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
\item{type}{Character. `"wordmark"` (default) uses the full wordmark.
|
||||
`"mark"` uses the compact C-pulse icon only.}
|
||||
|
||||
\item{variant}{Character. `"light"` (default) uses the dark logo for light
|
||||
backgrounds. `"dark"` uses the reverse (light) logo for dark backgrounds
|
||||
(pairs with [theme_civilytics_dark()]).}
|
||||
|
||||
\item{position}{Character. Corner placement for the logo: `"bottom-right"`
|
||||
(default), `"bottom-left"`, `"top-right"`, or `"top-left"`. Controls
|
||||
horizontal alignment of the logo grob.}
|
||||
}
|
||||
\value{
|
||||
a gg object which contains the logo file stored as a Grob suitable for manipulating in
|
||||
grid
|
||||
A ggplot object (class `"gg"`) containing the logo grob.
|
||||
}
|
||||
\description{
|
||||
Get a Civilytics Logo grob
|
||||
Returns a ggplot object containing the Civilytics logo as a rasterGrob,
|
||||
ready to compose with plots via [add_logo()], [add_logo_ga()], or the
|
||||
pipe-friendly [civilytics_logo()].
|
||||
}
|
||||
\examples{
|
||||
logo <- make_logo_grob()
|
||||
class(logo) # gg
|
||||
logo <- make_logo_grob() # wordmark, light
|
||||
logo <- make_logo_grob("mark", "dark") # mark, dark
|
||||
logo <- make_logo_grob(position = "bottom-left") # left-aligned
|
||||
class(logo) # "gg" "ggplot"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -17,6 +17,6 @@ the caption
|
||||
Measure a ggplot2 object caption
|
||||
}
|
||||
\examples{
|
||||
p1 <- ggplot2::qplot(mpg, wt, data = mtcars)
|
||||
measure_caption(p1) # Should equal 1 since no caption is required
|
||||
p1 <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point()
|
||||
measure_caption(p1) # Should equal 1 since no caption is present
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{na_sum}
|
||||
\alias{na_sum}
|
||||
\title{Sum a numeric that contains missing values and ignore missing values}
|
||||
\usage{
|
||||
na_sum(x, quiet = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{a numeric vector}
|
||||
|
||||
\item{quiet}{Logical. If \code{TRUE} (default \code{FALSE}), suppress the warning
|
||||
message. Useful when calling \code{na_sum()} inside a loop or \code{dplyr} pipeline
|
||||
where the message would be emitted repeatedly.}
|
||||
}
|
||||
\value{
|
||||
the sum, ignoring any missing values
|
||||
}
|
||||
\description{
|
||||
Sum a numeric that contains missing values and ignore missing values
|
||||
}
|
||||
\examples{
|
||||
x <- c(2, NA, 4, 9)
|
||||
na_sum(x) # 15
|
||||
na_sum(x, quiet = TRUE) # 15 (no message)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{outersect}
|
||||
\alias{outersect}
|
||||
\title{Compute the outersection of two fectors}
|
||||
\usage{
|
||||
outersect(x, y, ...)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{first vector, of any type}
|
||||
|
||||
\item{y}{second vector, same type as x}
|
||||
|
||||
\item{...}{additional vectors to be checked}
|
||||
}
|
||||
\value{
|
||||
unique values across all of the vectors
|
||||
}
|
||||
\description{
|
||||
Compute the outersection of two fectors
|
||||
}
|
||||
\examples{
|
||||
# desired result is c(1, 2, 3, 6, 9, 10)
|
||||
outersect(1:5, 4:8, 7:10)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{perturb_count}
|
||||
\alias{perturb_count}
|
||||
\title{Add a random jitter to a count variable to mask its true value}
|
||||
\usage{
|
||||
perturb_count(x, fac = 3)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{the vector of numerics}
|
||||
|
||||
\item{fac}{the range of values to add or subtract to perturb the count}
|
||||
}
|
||||
\value{
|
||||
a numeric vector
|
||||
}
|
||||
\description{
|
||||
Add a random jitter to a count variable to mask its true value
|
||||
}
|
||||
\details{
|
||||
The count in the name means that this function enforces a floor of
|
||||
0 on values, so values perturbed to have less than 0 will be capped at 0.
|
||||
}
|
||||
\examples{
|
||||
perturb_count(20:30, fac = 3)
|
||||
}
|
||||
@@ -21,6 +21,8 @@ a rasterImage
|
||||
Plot a jpeg image as a raster
|
||||
}
|
||||
\examples{
|
||||
img <- system.file("img","Civilytics Consulting Logo.jpg",package="civilytics")
|
||||
\dontrun{
|
||||
img <- system.file("img","Knowles_Headshot_2019_good.jpg",package="civilytics")
|
||||
plot_jpeg(img)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{postcode_lookup}
|
||||
\alias{postcode_lookup}
|
||||
\title{Title}
|
||||
\usage{
|
||||
postcode_lookup(x)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{a vector of state names}
|
||||
}
|
||||
\value{
|
||||
state abbreviations matching state naems provided in X
|
||||
}
|
||||
\description{
|
||||
Title
|
||||
}
|
||||
\examples{
|
||||
postcode_lookup("Montana")
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/quarto.R
|
||||
\name{quarto-helpers}
|
||||
\alias{quarto-helpers}
|
||||
\title{Install Civilytics Quarto Themes and Templates}
|
||||
\description{
|
||||
Helper functions that copy Civilytics Quarto assets from the installed
|
||||
package into your Quarto project directory. Logos are stored in a single
|
||||
canonical location (`inst/img/`) and copied to the paths expected by each
|
||||
template at install time.
|
||||
}
|
||||
@@ -9,6 +9,12 @@ race_short_names(x)
|
||||
\arguments{
|
||||
\item{x}{a character vector with NCES race codes, often from Urban Institute}
|
||||
}
|
||||
\value{
|
||||
recoded race categories following NCES race codes
|
||||
}
|
||||
\description{
|
||||
Recode NCES race categories to shorter names
|
||||
}
|
||||
\examples{
|
||||
race_short_names(c("Black", "Hispanic Or Latino", "Two Or More Races"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{random_round}
|
||||
\alias{random_round}
|
||||
\title{Add random noise to a variable before rounding}
|
||||
\usage{
|
||||
random_round(x)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{a numeric we want to round}
|
||||
}
|
||||
\value{
|
||||
rounded values
|
||||
}
|
||||
\description{
|
||||
Add random noise to a variable before rounding
|
||||
}
|
||||
\details{
|
||||
Credit to Jens von Bergmann for this algo https://github.com/mountainMath/dotdensity/blob/master/R/dot-density.R
|
||||
}
|
||||
\examples{
|
||||
random_round(1.93)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{rnh}
|
||||
\alias{rnh}
|
||||
\title{Round values to the nearest 0.5}
|
||||
\usage{
|
||||
rnh(x)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{a numeric vector to round}
|
||||
}
|
||||
\value{
|
||||
a numeric vector with all elements rounded to 0, 0.5, or 1
|
||||
}
|
||||
\description{
|
||||
Round values to the nearest 0.5
|
||||
}
|
||||
\examples{
|
||||
rnh(c(0.2, 0.3, 0.4, 0.8, 0.09, 0.9))
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{round_to_nearest_half}
|
||||
\alias{round_to_nearest_half}
|
||||
\title{Round values to the nearest 0.5}
|
||||
\usage{
|
||||
round_to_nearest_half(x)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{a numeric vector to round}
|
||||
}
|
||||
\value{
|
||||
a numeric vector with all elements rounded to 0, 0.5, or 1
|
||||
}
|
||||
\description{
|
||||
Round values to the nearest 0.5
|
||||
}
|
||||
\examples{
|
||||
round_to_nearest_half(0.9)
|
||||
round_to_nearest_half(0.7)
|
||||
round_to_nearest_half(0.4)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{safe_ratio}
|
||||
\alias{safe_ratio}
|
||||
\title{Safely take a ratio and do not fail if 0 is in the denominator}
|
||||
\usage{
|
||||
safe_ratio(num, denom)
|
||||
}
|
||||
\arguments{
|
||||
\item{num}{numerator, a numeric}
|
||||
|
||||
\item{denom}{denominator, a numeric}
|
||||
}
|
||||
\value{
|
||||
The proportion, safely calculated with 0.1 substituting for 0
|
||||
}
|
||||
\description{
|
||||
Safely take a ratio and do not fail if 0 is in the denominator
|
||||
}
|
||||
\examples{
|
||||
safe_ratio(100, 1)
|
||||
safe_ratio(100, 0)
|
||||
}
|
||||