diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..704e4aea --- /dev/null +++ b/.gitattributes @@ -0,0 +1,29 @@ +# Normalize line endings across platforms. +# Store text files with LF in the repo; check them out with LF everywhere. +* text=auto eol=lf + +# Explicit text files +*.R text eol=lf +*.Rmd text eol=lf +*.Rd text eol=lf +*.md text eol=lf +*.Rproj text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.csv text eol=lf +DESCRIPTION text eol=lf +NAMESPACE text eol=lf +LICENSE text eol=lf +.gitignore text eol=lf +.Rbuildignore text eol=lf + +# Binary files (never touch line endings) +*.rda binary +*.RData binary +*.rds binary +*.png binary +*.jpg binary +*.jpeg binary +*.ico binary +*.svg binary +*.pdf binary diff --git a/.github/workflows/check-standard.yaml b/.github/workflows/check-standard.yaml index 4b4be7d2..238e9447 100644 --- a/.github/workflows/check-standard.yaml +++ b/.github/workflows/check-standard.yaml @@ -1,91 +1,60 @@ -# Automatically checks the package on Windows and Mac each time master branch -# (or a pull request to master) gets a new commit. -# Based on . -# For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag. -# https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions +# Runs `R CMD check` on macOS, Windows, and Linux, across a matrix of R and +# ggplot2 versions, on pushes to main/master and on pull requests. +# Derived from https://github.com/r-lib/actions/tree/v2/examples (check-standard). on: push: - branches: - - main - - master + branches: [main, master] pull_request: - branches: - - main - - master name: R-CMD-check +permissions: read-all + jobs: R-CMD-check: runs-on: ${{ matrix.config.os }} - name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + name: ${{ matrix.config.os }} (R ${{ matrix.config.r }}${{ matrix.config.ggplot2 == 'devel' && ', ggplot2-dev' || '' }}) + + # The ggplot2-devel entry is a forward-compatibility canary: it surfaces + # breakage from upcoming ggplot2 releases (e.g. the `theme_set()` and + # `size`->`linewidth` issues) but is allowed to fail without blocking. + continue-on-error: ${{ matrix.config.allow_failure || false }} strategy: fail-fast: false matrix: config: + - {os: macos-latest, r: 'release'} - {os: windows-latest, r: 'release'} - - {os: macOS-latest, r: 'release'} - #- {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} - #- {os: ubuntu-20.04, r: 'devel', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} + - {os: ubuntu-latest, r: 'release'} + - {os: ubuntu-latest, r: 'oldrel-1'} + - {os: ubuntu-latest, r: 'release', ggplot2: 'devel', allow_failure: true} env: - R_REMOTES_NO_ERRORS_FROM_WARNINGS: true - RSPM: ${{ matrix.config.rspm }} + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 - uses: r-lib/actions/setup-r@v2 with: r-version: ${{ matrix.config.r }} + http-user-agent: ${{ matrix.config.http-user-agent }} + use-public-rspm: true - - uses: r-lib/actions/setup-pandoc@v2 - - - name: Query dependencies - run: | - install.packages('remotes') - saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) - writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") - shell: Rscript {0} - - - name: Cache R packages - if: runner.os != 'Windows' - uses: actions/cache@v3 + - uses: r-lib/actions/setup-r-dependencies@v2 with: - path: ${{ env.R_LIBS_USER }} - key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} - restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- - - #- name: Install system dependencies - # if: runner.os == 'Linux' - # run: | - # while read -r cmd - # do - # eval sudo $cmd - # done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))') - - - name: Install XQuartz on macOS - if: runner.os == 'macOS' - run: | - brew install --cask xquartz - - - name: Install dependencies - run: | - remotes::install_deps(dependencies = TRUE) - remotes::install_cran("rcmdcheck") - shell: Rscript {0} - - - name: Check - env: - _R_CHECK_CRAN_INCOMING_REMOTE_: false - run: rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "error", check_dir = "check") - shell: Rscript {0} + extra-packages: >- + any::rcmdcheck + ${{ matrix.config.ggplot2 == 'devel' && 'github::tidyverse/ggplot2' || '' }} + needs: check - - name: Upload check results - if: failure() - uses: actions/upload-artifact@main + - uses: r-lib/actions/check-r-package@v2 with: - name: ${{ runner.os }}-r${{ matrix.config.r }}-results - path: check + args: 'c("--no-manual", "--as-cran")' + error-on: '"error"' + upload-snapshots: true diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 390d2338..c4602bf6 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -15,14 +15,14 @@ jobs: env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-r@v2 - uses: r-lib/actions/setup-pandoc@v2 - name: Checkout CMAP fonts repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: CMAP-REPOS/cmap-fonts token: ${{ secrets.CMAP_REPO_FULL_ACCESS }} @@ -47,7 +47,7 @@ jobs: shell: Rscript {0} - name: Cache R packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.R_LIBS_USER }} key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} diff --git a/DESCRIPTION b/DESCRIPTION index 50497840..2101944a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -36,7 +36,7 @@ Encoding: UTF-8 LazyData: true Depends: R (>= 3.5.0), - ggplot2 + ggplot2 (>= 3.4.0) Imports: dplyr, generics, @@ -51,7 +51,6 @@ Imports: lubridate, purrr, ragg, - Rcpp, rlang, rstudioapi, scales, diff --git a/NAMESPACE b/NAMESPACE index 1d99fa48..d6ffb712 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -39,7 +39,6 @@ export(unapply_cmap_default_aes) export(update_recessions) export(viz_gradient) export(viz_palette) -import(Rcpp) import(dplyr) import(ggplot2) import(ggrepel) diff --git a/R/cmapplot.R b/R/cmapplot.R index dc5ea71a..73bfb72c 100644 --- a/R/cmapplot.R +++ b/R/cmapplot.R @@ -11,7 +11,7 @@ #' #' @name cmapplot #' @docType package -#' @import dplyr ggplot2 graphics grDevices grid gridtext Rcpp ragg rlang scales systemfonts +#' @import dplyr ggplot2 graphics grDevices grid gridtext ragg rlang scales systemfonts #' @importFrom glue glue glue_collapse #' @keywords internal "_PACKAGE" @@ -42,13 +42,25 @@ whitney_paths <- dplyr::filter(systemfonts::system_fonts(), family == "Whitney") whitney_paths <- whitney_paths[["path"]] - # On some OSX systems (e.g. pkgdown GHA VM) system_fonts() cannot find fonts - # installed in the user fonts directory. In any case where system_fonts() - # sees no Whitney fonts, if `user_dir` exists, it too is checked for fonts. - user_dir <- paste0(Sys.getenv("HOME"), "/Library/Fonts") - if(length(whitney_paths) == 0 & dir.exists(user_dir)){ - whitney_paths <- list.files(user_dir, full.names = TRUE) - whitney_paths <- grep("Whitney-", whitney_paths, value = TRUE) + # On some systems (e.g. the pkgdown GHA VM) system_fonts() cannot find fonts + # installed in a user fonts directory. Where system_fonts() finds no Whitney + # fonts, also search the platform's user font directories directly: + # macOS: ~/Library/Fonts + # Windows: %LOCALAPPDATA%/Microsoft/Windows/Fonts + # Linux: ~/.local/share/fonts and ~/.fonts + if(length(whitney_paths) == 0){ + user_font_dirs <- c( + file.path(Sys.getenv("HOME"), "Library", "Fonts"), + file.path(Sys.getenv("LOCALAPPDATA"), "Microsoft", "Windows", "Fonts"), + file.path(Sys.getenv("HOME"), ".local", "share", "fonts"), + file.path(Sys.getenv("HOME"), ".fonts") + ) + user_font_dirs <- user_font_dirs[dir.exists(user_font_dirs)] + if(length(user_font_dirs) > 0){ + whitney_paths <- grep("Whitney-", + list.files(user_font_dirs, full.names = TRUE), + value = TRUE) + } } # Register preferred fonts using the paths found above. This will only be @@ -107,10 +119,18 @@ light = list(family = cmapplot_globals$preferred_font$light, face = "plain")), envir = cmapplot_globals) - # ... and check on rstudio graphics - if (rstudioapi::isAvailable()){ - if(rstudioapi::getVersion() > "1.4"){ - if(getOption("RStudioGD.backend", FALSE) != "ragg"){ + # ... and set up the graphics backend so Whitney renders in the plot window. + # Detect the IDE via environment variables rather than rstudioapi: Positron + # ships a partial rstudioapi shim, so isAvailable() returns TRUE there and + # getVersion() can error outside RStudio. RStudio sets RSTUDIO=1; Positron + # sets POSITRON_VERSION. + in_rstudio <- Sys.getenv("RSTUDIO") == "1" + in_positron <- Sys.getenv("POSITRON_VERSION") != "" + + if (in_rstudio) { + # RStudio >= 1.4 can render registered fonts via the ragg backend. + if (rstudioapi::isAvailable() && rstudioapi::getVersion() > "1.4") { + if (!identical(getOption("RStudioGD.backend"), "ragg")) { options(RStudioGD.backend = "ragg") packageStartupMessage(paste( "cmapplot has set RStudio graphics to `ragg` for the current session.", @@ -123,12 +143,19 @@ "cmapplot requires RStudio v1.4 or greater to use Whitney fonts", "in the R plots window.\nPlease update RStudio.")) } - # If using vanilla R, encourage RStudio installation + } else if (in_positron) { + # Positron's graphics device automatically uses ragg when it is installed. + # ragg is a hard dependency of cmapplot, so no backend option is needed. + packageStartupMessage( + "cmapplot will use Positron's graphics device to render Whitney fonts." + ) + # If using vanilla R (no supported IDE), note the limitation. } else { packageStartupMessage(paste( - "cmapplot requires RStudio to use Whitney fonts in the R plots window.\n ", - "Please install RStudio. ")) + "cmapplot renders Whitney fonts in the plot window when run in RStudio or Positron.\n ", + "In plain R, use finalize_plot()'s export modes to produce graphics with Whitney fonts.")) } + # Otherwise, notify user } else { packageStartupMessage( diff --git a/R/default_aes.R b/R/default_aes.R index ada2ebb3..2b3ba83a 100644 --- a/R/default_aes.R +++ b/R/default_aes.R @@ -15,7 +15,7 @@ init_cmap_default_aes <- function() { colour = cmapplot_globals$colors$blackish ), Line = list( - size = gg_lwd_convert(cmapplot_globals$consts$lwd_plotline) + linewidth = gg_lwd_convert(cmapplot_globals$consts$lwd_plotline) ), Text = list( family = cmapplot_globals$font$strong$family, diff --git a/R/finalize_plot.R b/R/finalize_plot.R index 03697102..b9f803ad 100644 --- a/R/finalize_plot.R +++ b/R/finalize_plot.R @@ -256,6 +256,14 @@ finalize_plot <- function(plot = NULL, caption <- input_caption } + # gridtext parses `title` and `caption` as markdown. A newline followed by + # indentation is read as an indented code block (an unsupported
 tag),
+  # which errors in textbox_grob(). Collapse any newline plus surrounding
+  # spaces/tabs to a single space so multi-line strings (often indented in
+  # source code) render as continuous text. Use 
for explicit line breaks. + title <- gsub("[ \t]*\n[ \t]*", " ", title) + caption <- gsub("[ \t]*\n[ \t]*", " ", caption) + # Build necessary grobs ----------------------------------------------------- grobs <- list() diff --git a/R/geom_pandemics.R b/R/geom_pandemics.R index 95250313..bb54721e 100644 --- a/R/geom_pandemics.R +++ b/R/geom_pandemics.R @@ -262,7 +262,7 @@ GeomPandemics <- ggproto( default_aes = aes( colour = NA, alpha = 0.11, - size = 0.5, + linewidth = 0.5, linetype = 1, na.rm = TRUE, show.legend = FALSE @@ -334,7 +334,7 @@ GeomPandemics <- ggproto( gp = gpar( col = coords$colour, fill = alpha(coords$fill, coords$alpha), - lwd = coords$size * .pt, + lwd = coords$linewidth * .pt, lty = coords$linetype, linejoin = linejoin, # `lineend` is a workaround for Windows and intentionally kept unexposed diff --git a/R/geom_recessions.R b/R/geom_recessions.R index 70aa9f23..664b708d 100644 --- a/R/geom_recessions.R +++ b/R/geom_recessions.R @@ -1,525 +1,525 @@ -#'Add recessions to time series graphs -#' -#'\code{geom_recessions} returns one or two ggplot geoms that add rectangles -#'representing recessions to a plot. It will either return only rectangles or, -#'by default, both rectangles and text identifying each recession. -#' -#'@param xformat Char, a string indicating whether the x axis of the primary -#' data being graphed is in integer or date format. This argument will -#' currently accept one of \code{c("numeric", "date")}. -#'@param text Logical, whether or not to include labels that identify each box -#' as a recession. -#'@param label Char, the text to label each recession. Defaults to " Recession". -#' (The space is a more consistent y axix buffer than text_nudge_y because it -#' not relative to the scale of the y axis.) -#'@param ymin,ymax Numeric, The height of the recession rectangles. Defaults to -#' -Inf and +Inf. Override to the top and bottom gridlines to implement ideal -#' CMAP design standards. -#'@param fill Char, the fill color for the recession rectangles. Defaults to -#' \code{#002d49} for compliance with CMAP design standards. -#'@param text_nudge_x,text_nudge_y Numeric, the amount to shift the labels along -#' each axis. Defaults to 0.2 and 0, respectively. Note that these use the x -#' and y scales so will need to be adjusted depending on what is being graphed. -#' `text_nudge_y` only works when `ymax` is not set to `+Inf`, which is the -#' default. Consider setting `ymax` equal to the top of your graph or top -#' gridline as an additional argument in `geom_recessions()`. -#'@param show.legend Logical, whether to render the rectangles in the legend. -#' Defaults to \code{FALSE}. -#'@param rect_aes,text_aes Named list, additional aesthetics to send to the -#' rectangle and text geoms, respectively. -#'@param update_recessions Logical or data frame. \code{FALSE}, the default, -#' relies on the package's built in recessions table, which was last updated in -#' March 2021 and is loaded into the \code{sysdata.R} file located in the -#' \code{R} directory. \code{TRUE} calls the function -#' \code{update_recessions}, which attempts to fetch the current recessions -#' table from the NBER website. A custom data table of recessions can also be -#' passed to this argument, but it must be structured identically to the -#' five-column data table described in the the documentation file for the -#' function \code{update_recessions}. -#'@param show_ongoing Logical. \code{TRUE}, the default, will display an ongoing -#' recession that does not yet have a defined end date. If an ongoing recession -#' exists, it will be displayed as extending through the maximum extent of the -#' graph's data (up to 2200). \code{FALSE} will remove the ongoing recession -#' from the graph. -#'@param ... additional aesthetics to send to BOTH the rectangle and text geoms. -#' -#'@section Important notes: If \code{show.legend = TRUE} you must place any -#' categorical aesthetics (e.g. color, size) specific to the primary data in -#' the geom(s) used to display that data. Otherwise, the legend will inherit -#' aesthetics from geom_recessions. -#' -#' It is best to place this object before your primary geom (likely -#' \code{geom_line()}) in your code, so that ggplot draws it behind the primary -#' data being drawn. -#' -#'@section Default color: The CMAP color palette gray used for recessions is -#' \code{#e3e8eb}. The rectangle geom has default fill and alpha values of -#' \code{#002d49} and \code{0.11} built into the function. These replicate the -#' palette color at the highest possible transparency. This is done because -#' there is no known way to place the recession geom behind the graph's grid -#' lines. The default therefore produces the approved CMAP color while altering -#' the appearance of any overlapping grid lines as little as possible. These -#' can be overridden, but separately. Override fill using the top-level -#' argument, as in \code{fill = "red"}. Override alpha within rect_aes as in -#' \code{rect_aes = list(alpha = 0.5)}. Color and alpha were calculated using -#' the hints found here: -#' \url{https://stackoverflow.com/questions/6672374/convert-rgb-to-rgba-over-white}. -#' -#' -#'@section Under the hood: This function calls two custom geoms, constructed -#' with ggproto. The custom GeomRecessions and GeomRecessionsText are modified -#' versions of GeomRect and GeomText, respectively. The only variations to each -#' occur in \code{default_aes}, \code{required_aes}, and \code{setup_data} -#' arguments. These variations allow the the primary dataframe (specified in -#' \code{ggplot(data = XXX)}) to filter the recessions displayed. -#' -#' @examples -#' grp_goods <- dplyr::filter(grp_over_time, category == "Goods-Producing") -#' grp_goods <- dplyr::mutate(grp_goods, year2 = as.Date(lubridate::date_decimal(year))) -#' -#' # INTEGER X AXIS: -#' ggplot(grp_over_time, aes(x = year, y = realgrp, color = cluster)) + -#' geom_recessions() + -#' geom_line() + -#' scale_x_continuous("Year") + -#' theme_minimal() -#' -#' # DATE X AXIS: -#' ggplot(data = grp_goods, -#' mapping = aes(x = year2, y = realgrp, color = cluster)) + -#' geom_recessions(xformat = "date") + -#' geom_line() + -#' scale_x_date("Year") + -#' theme_minimal() -#' -#' # MODIFIED AESTHETICS: -#' ggplot(grp_over_time, aes(x = year, y = realgrp)) + -#' geom_recessions(show.legend = TRUE, fill = "blue", text = FALSE, -#' rect_aes = list(alpha = 1, color = "red")) + -#' geom_line(aes(color = cluster)) + -#' scale_x_continuous("Year") + -#' theme_minimal() -#' -#' -#' # BELOW EXAMPLES SHOW MORE THAN 1 RECESSION -#' df <- data.frame(year_dec=1950:1999, value=rnorm(100), var=c(rep("A", 50), rep("B", 50))) -#' df$year_date <- as.Date(lubridate::date_decimal(df$year_dec)) -#' -#' # A plot with an integer-based x axis -#' ggplot(df, mapping = aes(x = year_dec, y = value)) + -#' geom_recessions() + -#' geom_line(aes(color = var)) + -#' scale_x_continuous("Year") + -#' theme_minimal() -#' -#' # A plot with a date-based x axis -#' ggplot(df, mapping = aes(x = year_date, y = value)) + -#' geom_recessions(xformat = "date", show.legend = TRUE) + -#' geom_line(aes(color = var)) + -#' scale_x_date() + -#' theme_minimal() -#' -#'@importFrom utils read.csv -#' -#'@seealso \itemize{ \item \url{https://ggplot2-book.org/extensions.html} \item -#' \url{https://github.com/brodieG/ggbg/blob/development/inst/doc/extensions.html#stat-compute} -#' \item \url{https://rpubs.com/hadley/97970} \item -#' \url{https://ggplot2.tidyverse.org/articles/extending-ggplot2.html} } -#' -#'@export -geom_recessions <- function(xformat = "numeric", - text = TRUE, - label = " Recession", - ymin = -Inf, - ymax = Inf, - fill = "#002d49", - text_nudge_x = 0.2, - text_nudge_y = 0, - show.legend = FALSE, - rect_aes = NULL, - text_aes = NULL, - update_recessions = FALSE, - show_ongoing = TRUE, - ...) { - - # build recessions table for use in function, but hide it in a list - # because of ggplot's requirement that parameters be of length 1 - recess_table <- list(build_recessions(update_recessions)) - - # return a series of gg objects to ggplot - list( - layer( - data = NULL, - # An aesthetic mapping is required to generate a legend entry - mapping = aes(fill = "Recession"), - stat = "identity", - geom = GeomRecessions, - position = "identity", - show.legend = if_else(show.legend, NA, FALSE), - inherit.aes = TRUE, - params = append( - list( - xformat = xformat, - ymin = ymin, - ymax = ymax, - recess_table = recess_table, - show_ongoing = show_ongoing, - ... - ), - rect_aes - ) - ), - if (text) { - layer( - data = NULL, - mapping = NULL, - stat = "identity", - geom = GeomRecessionsText, - position = position_nudge(x = text_nudge_x, y = 0), - show.legend = FALSE, - inherit.aes = TRUE, - params = append( - list( - xformat = xformat, - label = label, - y = ymax + text_nudge_y, - # Because ymax is Inf by default, adjustments to this setting - # require manually setting `ymax` in the call to `geom_recessions` - recess_table = recess_table, - show_ongoing = show_ongoing, - ... - ), - text_aes - ) - ) - }, - # apply the fill color to values where fill="Recession". Push this to the legend if called for. - do.call( - scale_fill_manual, - list(values = c("Recession" = fill), if (show.legend) {guide = "legend"}) - ) - ) - -} - - -# internal function used to define recessions table for use -build_recessions <- function(update_recessions){ - if(is.logical(update_recessions)){ - # if TRUE - if(update_recessions){ - message("Trying to update recessions...") - updated_recessions <- suppressWarnings(update_recessions(quietly = TRUE)) - - # If updated_recessions is returned as NULL, use the default table - if (is.null(updated_recessions)) { - message("Could not update recessions. Using built-in recessions table...") - return(recessions) - } - - message("Successfully fetched from NBER") - return(updated_recessions) - # if FALSE - } else { - return(recessions) - } - # if DATAFRAME - }else if(is.data.frame(update_recessions)){ - # confirm that table has correct structure - if(!identical(update_recessions[NA,][1,], recessions[NA,][1,])){ - message("Recession table may not have correct format (See `?update_recessions`). Attempting anyway...") - } - return(update_recessions) - # OTHERWISE - }else{ - message("`update_recessions` must be TRUE, FALSE, or a data frame. Using built-in recessions table...") - return(recessions) - } -} - -#' @importFrom lubridate decimal_date - -# Internal function designed to filter the built-in recessions table -filter_recessions <- function(min, max, xformat, show_ongoing, recess_table){ - # Bind local variables to function - end_num <- start_num <- end_date <- start_date <- end <- start <- ongoing <- NULL - - # unwrap recess_table from list - recess_table <- recess_table[[1]] - - # Filtering out ongoing recessions if specified - if (!show_ongoing) {recess_table <- dplyr::filter(recess_table, ongoing == F)} - - # use xformat to create correct "start" and "end" vars... - if (xformat == "date") { - # ... by renaming existing date fields (for date axis) - recessions <- dplyr::rename(recess_table, start = start_date, end = end_date) - } else { - # ... or by creating decimal dates (for numeric axis) - if (xformat != "numeric") { - warning("geom_recessions currently only supports x axes in the numeric and date formats. Using numeric.") - } - recessions <- dplyr::mutate( - recess_table, - start = lubridate::decimal_date(start_date), - end = lubridate::decimal_date(end_date) - ) - } - - # Remove recessions outside of range - recessions <- dplyr::filter(recessions, end > min & start < max) - - # If `min` or `max` fall in middle of a recession, modify recession to end at specified term. - recessions <- dplyr::transmute( - recessions, - start = if_else(start < min, min, as.numeric(start)), - end = if_else(end > max, max, as.numeric(end)), - ) - - return(recessions) -} - - -#' @name customproto -NULL - -#' @describeIn customproto Add recession bars to plot. -#' @format NULL -#' @usage NULL -#' @export -GeomRecessions <- ggproto( - "GeomRecessions", Geom, - default_aes = aes(colour = NA, alpha = 0.11, size = 0.5, linetype = 1, na.rm = TRUE), - - required_aes = c("xformat", "ymin", "ymax", "show_ongoing", "recess_table" ,"fill"), - - # replace `data` with `recessions`, filtered by `data` - setup_data = function(data, params) { - #filter recessions based on date parameters from `data` and return it. This overwrites `data`. - data <- filter_recessions(min = min(data$x), max = max(data$x), - xformat = params$xformat, - show_ongoing = params$show_ongoing, - recess_table = params$recess_table) - - # set up data for GeomRect - data <- dplyr::transmute( - data, - xmin = start, - xmax = end, - ymin = params$ymin, - ymax = params$ymax, - PANEL = 1, - group = -1, - # re-establish dummy aesthetic, needed for legend entry - fill = "Recession" - ) - - return(data) - }, - - # remainder untouched from `geom_rect`: - draw_panel = function(self, data, panel_params, coord, linejoin = "mitre") { - - if (!coord$is_linear()) { - aesthetics <- setdiff( - names(data), c("x", "y", "xmin", "xmax", "ymin", "ymax") - ) - - polys <- lapply(split(data, seq_len(nrow(data))), function(row) { - poly <- rect_to_poly(row$xmin, row$xmax, row$ymin, row$ymax) - aes <- new_data_frame(row[aesthetics])[rep(1,5), ] - - GeomPolygon$draw_panel(cbind(poly, aes), panel_params, coord) - }) - - ggname("bar", do.call("grobTree", polys)) - } else { - coords <- coord$transform(data, panel_params) - ggname("geom_rect", rectGrob( - coords$xmin, coords$ymax, - width = coords$xmax - coords$xmin, - height = coords$ymax - coords$ymin, - default.units = "native", - just = c("left", "top"), - gp = gpar( - col = coords$colour, - fill = alpha(coords$fill, coords$alpha), - lwd = coords$size * .pt, - lty = coords$linetype, - linejoin = linejoin, - # `lineend` is a workaround for Windows and intentionally kept unexposed - # as an argument. (c.f. https://github.com/tidyverse/ggplot2/issues/3037#issuecomment-457504667) - lineend = if (identical(linejoin, "round")) "round" else "square" - ) - )) - } - }, - - draw_key = draw_key_polygon -) - - -#' @describeIn customproto Add recession bar labels to plot. -#' @format NULL -#' @usage NULL -#' @export -GeomRecessionsText <- ggproto( - "GeomRecessionsText", Geom, - - required_aes = c("xformat", "label", "show_ongoing", "recess_table", "y"), - - default_aes = aes( - colour = "black", size = 3.88, alpha = NA, family = "", fontface = 1, lineheight = 1.2, - xformat = NULL, angle = 270, parse = FALSE, - check_overlap = FALSE, na.rm = TRUE, - hjust = "left", vjust = "bottom" - ), - - # replace `data` with `recessions`, filtered by `data` - setup_data = function(data, params) { - #filter recessions based on date parameters from `data` and return it. This overwrites `data`. - data <- filter_recessions(min = min(data$x), max = max(data$x), - xformat = params$xformat, - show_ongoing = params$show_ongoing, - recess_table = params$recess_table) - - # set up data for GeomRect - data <- dplyr::transmute( - data, - x = end, - y = params$y, - PANEL = 1, - group = -1 - ) - - return(data) - }, - - # remainder untouched from `geom_text`: - draw_panel = function(data, panel_params, coord, parse = FALSE, - na.rm = FALSE, check_overlap = FALSE) { - lab <- data$label - if (parse) { - lab <- parse_safe(as.character(lab)) - } - - data <- coord$transform(data, panel_params) - if (is.character(data$vjust)) { - data$vjust <- compute_just(data$vjust, data$y) - } - if (is.character(data$hjust)) { - data$hjust <- compute_just(data$hjust, data$x) - } - - textGrob( - lab, - data$x, data$y, default.units = "native", - hjust = data$hjust, vjust = data$vjust, - rot = data$angle, - gp = gpar( - col = alpha(data$colour, data$alpha), - fontsize = data$size * .pt, - fontfamily = data$family, - fontface = data$fontface, - lineheight = data$lineheight - ), - check.overlap = check_overlap - ) - }, - - draw_key = draw_key_text -) - - -#'Update recessions table -#' -#'The cmapplot package contains an internal dataset \code{recessions} of all -#'recessions in American history as recorded by the National Bureau of Economic -#'Research (NBER). However, users may need to replace the built-in data, such as -#'in the event of new recessions and/or changes to the NBER consensus on -#'recession dates. This function fetches and interprets this data from the NBER -#'website. -#' -#'@param url Char, the web location of the NBER machine-readable CSV file. The -#' default, \code{NULL}, uses the most recently identified URL known to the -#' package development team, which appears to be the most stable location for -#' updates over time. -#'@param quietly Logical, suppresses messages produced by -#' \code{utils::download.file}. -#' -#'@return A data frame with the following variables: \itemize{ \item -#' \code{start_char, end_char}: Chr. Easily readable labels for the beginning -#' and end of the recession. \item \code{start_date, end_date}: Date. Dates -#' expressed in R datetime format, using the first day of the specified month. -#' \item \code{ongoing}: Logical. Whether or not the recession is ongoing as of -#' the latest available NBER data. } -#' -#'@source \url{https://www.nber.org/data/cycles/cycle dates pasted.csv} -#' -#' @examples -#' recessions <- update_recessions() -#' -#' # package maintainers can update the internal dataset from within -#' # package by running the following code: -#' \dontrun{ -#' recessions <- update_recessions() -#' usethis::use_data(recessions, internal = TRUE, overwrite = TRUE) -#' } -#' -#'@export -update_recessions <- function(url = NULL, quietly = FALSE){ - - # Use default URL if user does not override - if (is_null(url) | missing(url)) { - url <- "http://data.nber.org/data/cycles/20210719_cycle_dates_pasted.csv" - } - - # locally bind variable names - start_char <- end_char <- start_date <- end_date <- ongoing <- index <- peak <- trough <- NULL - - return( - # attempt to download and format recessions table - tryCatch({ - recessions <- read.csv(url) %>% - # drop first row trough - dplyr::slice(-1) %>% - # convert peaks and troughs... - dplyr::mutate( - # ...to R dates - start_date = as.Date(peak), - end_date = as.Date(trough), - # ... and clean char strings - start_char = format(start_date, "%b %Y"), - end_char = format(end_date, "%b %Y")) %>% - # confirm ascending and create row number - dplyr::arrange(start_date) %>% - mutate(index = row_number()) %>% - mutate( - # Flag unfinished recessions - ongoing = case_when( - is.na(end_date) & index == max(.$index) ~ T, - TRUE ~ F), - # set ongoing recession to arbitrary future date - end_date = case_when( - ongoing ~ as.Date("2500-01-01"), - TRUE ~ end_date), - # mark ongoing recession in char field - end_char = case_when( - ongoing ~ "Ongoing", - TRUE ~ end_char) - ) %>% - # clean up - select(start_char, end_char, start_date, end_date, ongoing) - - if (!quietly) {message("Successfully fetched from NBER")} - - # Return recessions - recessions - }, - error = function(cond){ - if (!quietly) message("WARNING: Fetch or processing failed. `NULL` returned.") - return(NULL) - } - ) - ) -} +#'Add recessions to time series graphs +#' +#'\code{geom_recessions} returns one or two ggplot geoms that add rectangles +#'representing recessions to a plot. It will either return only rectangles or, +#'by default, both rectangles and text identifying each recession. +#' +#'@param xformat Char, a string indicating whether the x axis of the primary +#' data being graphed is in integer or date format. This argument will +#' currently accept one of \code{c("numeric", "date")}. +#'@param text Logical, whether or not to include labels that identify each box +#' as a recession. +#'@param label Char, the text to label each recession. Defaults to " Recession". +#' (The space is a more consistent y axix buffer than text_nudge_y because it +#' not relative to the scale of the y axis.) +#'@param ymin,ymax Numeric, The height of the recession rectangles. Defaults to +#' -Inf and +Inf. Override to the top and bottom gridlines to implement ideal +#' CMAP design standards. +#'@param fill Char, the fill color for the recession rectangles. Defaults to +#' \code{#002d49} for compliance with CMAP design standards. +#'@param text_nudge_x,text_nudge_y Numeric, the amount to shift the labels along +#' each axis. Defaults to 0.2 and 0, respectively. Note that these use the x +#' and y scales so will need to be adjusted depending on what is being graphed. +#' `text_nudge_y` only works when `ymax` is not set to `+Inf`, which is the +#' default. Consider setting `ymax` equal to the top of your graph or top +#' gridline as an additional argument in `geom_recessions()`. +#'@param show.legend Logical, whether to render the rectangles in the legend. +#' Defaults to \code{FALSE}. +#'@param rect_aes,text_aes Named list, additional aesthetics to send to the +#' rectangle and text geoms, respectively. +#'@param update_recessions Logical or data frame. \code{FALSE}, the default, +#' relies on the package's built in recessions table, which was last updated in +#' March 2021 and is loaded into the \code{sysdata.R} file located in the +#' \code{R} directory. \code{TRUE} calls the function +#' \code{update_recessions}, which attempts to fetch the current recessions +#' table from the NBER website. A custom data table of recessions can also be +#' passed to this argument, but it must be structured identically to the +#' five-column data table described in the the documentation file for the +#' function \code{update_recessions}. +#'@param show_ongoing Logical. \code{TRUE}, the default, will display an ongoing +#' recession that does not yet have a defined end date. If an ongoing recession +#' exists, it will be displayed as extending through the maximum extent of the +#' graph's data (up to 2200). \code{FALSE} will remove the ongoing recession +#' from the graph. +#'@param ... additional aesthetics to send to BOTH the rectangle and text geoms. +#' +#'@section Important notes: If \code{show.legend = TRUE} you must place any +#' categorical aesthetics (e.g. color, size) specific to the primary data in +#' the geom(s) used to display that data. Otherwise, the legend will inherit +#' aesthetics from geom_recessions. +#' +#' It is best to place this object before your primary geom (likely +#' \code{geom_line()}) in your code, so that ggplot draws it behind the primary +#' data being drawn. +#' +#'@section Default color: The CMAP color palette gray used for recessions is +#' \code{#e3e8eb}. The rectangle geom has default fill and alpha values of +#' \code{#002d49} and \code{0.11} built into the function. These replicate the +#' palette color at the highest possible transparency. This is done because +#' there is no known way to place the recession geom behind the graph's grid +#' lines. The default therefore produces the approved CMAP color while altering +#' the appearance of any overlapping grid lines as little as possible. These +#' can be overridden, but separately. Override fill using the top-level +#' argument, as in \code{fill = "red"}. Override alpha within rect_aes as in +#' \code{rect_aes = list(alpha = 0.5)}. Color and alpha were calculated using +#' the hints found here: +#' \url{https://stackoverflow.com/questions/6672374/convert-rgb-to-rgba-over-white}. +#' +#' +#'@section Under the hood: This function calls two custom geoms, constructed +#' with ggproto. The custom GeomRecessions and GeomRecessionsText are modified +#' versions of GeomRect and GeomText, respectively. The only variations to each +#' occur in \code{default_aes}, \code{required_aes}, and \code{setup_data} +#' arguments. These variations allow the the primary dataframe (specified in +#' \code{ggplot(data = XXX)}) to filter the recessions displayed. +#' +#' @examples +#' grp_goods <- dplyr::filter(grp_over_time, category == "Goods-Producing") +#' grp_goods <- dplyr::mutate(grp_goods, year2 = as.Date(lubridate::date_decimal(year))) +#' +#' # INTEGER X AXIS: +#' ggplot(grp_over_time, aes(x = year, y = realgrp, color = cluster)) + +#' geom_recessions() + +#' geom_line() + +#' scale_x_continuous("Year") + +#' theme_minimal() +#' +#' # DATE X AXIS: +#' ggplot(data = grp_goods, +#' mapping = aes(x = year2, y = realgrp, color = cluster)) + +#' geom_recessions(xformat = "date") + +#' geom_line() + +#' scale_x_date("Year") + +#' theme_minimal() +#' +#' # MODIFIED AESTHETICS: +#' ggplot(grp_over_time, aes(x = year, y = realgrp)) + +#' geom_recessions(show.legend = TRUE, fill = "blue", text = FALSE, +#' rect_aes = list(alpha = 1, color = "red")) + +#' geom_line(aes(color = cluster)) + +#' scale_x_continuous("Year") + +#' theme_minimal() +#' +#' +#' # BELOW EXAMPLES SHOW MORE THAN 1 RECESSION +#' df <- data.frame(year_dec=1950:1999, value=rnorm(100), var=c(rep("A", 50), rep("B", 50))) +#' df$year_date <- as.Date(lubridate::date_decimal(df$year_dec)) +#' +#' # A plot with an integer-based x axis +#' ggplot(df, mapping = aes(x = year_dec, y = value)) + +#' geom_recessions() + +#' geom_line(aes(color = var)) + +#' scale_x_continuous("Year") + +#' theme_minimal() +#' +#' # A plot with a date-based x axis +#' ggplot(df, mapping = aes(x = year_date, y = value)) + +#' geom_recessions(xformat = "date", show.legend = TRUE) + +#' geom_line(aes(color = var)) + +#' scale_x_date() + +#' theme_minimal() +#' +#'@importFrom utils read.csv +#' +#'@seealso \itemize{ \item \url{https://ggplot2-book.org/extensions.html} \item +#' \url{https://github.com/brodieG/ggbg/blob/development/inst/doc/extensions.html#stat-compute} +#' \item \url{https://rpubs.com/hadley/97970} \item +#' \url{https://ggplot2.tidyverse.org/articles/extending-ggplot2.html} } +#' +#'@export +geom_recessions <- function(xformat = "numeric", + text = TRUE, + label = " Recession", + ymin = -Inf, + ymax = Inf, + fill = "#002d49", + text_nudge_x = 0.2, + text_nudge_y = 0, + show.legend = FALSE, + rect_aes = NULL, + text_aes = NULL, + update_recessions = FALSE, + show_ongoing = TRUE, + ...) { + + # build recessions table for use in function, but hide it in a list + # because of ggplot's requirement that parameters be of length 1 + recess_table <- list(build_recessions(update_recessions)) + + # return a series of gg objects to ggplot + list( + layer( + data = NULL, + # An aesthetic mapping is required to generate a legend entry + mapping = aes(fill = "Recession"), + stat = "identity", + geom = GeomRecessions, + position = "identity", + show.legend = if_else(show.legend, NA, FALSE), + inherit.aes = TRUE, + params = append( + list( + xformat = xformat, + ymin = ymin, + ymax = ymax, + recess_table = recess_table, + show_ongoing = show_ongoing, + ... + ), + rect_aes + ) + ), + if (text) { + layer( + data = NULL, + mapping = NULL, + stat = "identity", + geom = GeomRecessionsText, + position = position_nudge(x = text_nudge_x, y = 0), + show.legend = FALSE, + inherit.aes = TRUE, + params = append( + list( + xformat = xformat, + label = label, + y = ymax + text_nudge_y, + # Because ymax is Inf by default, adjustments to this setting + # require manually setting `ymax` in the call to `geom_recessions` + recess_table = recess_table, + show_ongoing = show_ongoing, + ... + ), + text_aes + ) + ) + }, + # apply the fill color to values where fill="Recession". Push this to the legend if called for. + do.call( + scale_fill_manual, + list(values = c("Recession" = fill), if (show.legend) {guide = "legend"}) + ) + ) + +} + + +# internal function used to define recessions table for use +build_recessions <- function(update_recessions){ + if(is.logical(update_recessions)){ + # if TRUE + if(update_recessions){ + message("Trying to update recessions...") + updated_recessions <- suppressWarnings(update_recessions(quietly = TRUE)) + + # If updated_recessions is returned as NULL, use the default table + if (is.null(updated_recessions)) { + message("Could not update recessions. Using built-in recessions table...") + return(recessions) + } + + message("Successfully fetched from NBER") + return(updated_recessions) + # if FALSE + } else { + return(recessions) + } + # if DATAFRAME + }else if(is.data.frame(update_recessions)){ + # confirm that table has correct structure + if(!identical(update_recessions[NA,][1,], recessions[NA,][1,])){ + message("Recession table may not have correct format (See `?update_recessions`). Attempting anyway...") + } + return(update_recessions) + # OTHERWISE + }else{ + message("`update_recessions` must be TRUE, FALSE, or a data frame. Using built-in recessions table...") + return(recessions) + } +} + +#' @importFrom lubridate decimal_date + +# Internal function designed to filter the built-in recessions table +filter_recessions <- function(min, max, xformat, show_ongoing, recess_table){ + # Bind local variables to function + end_num <- start_num <- end_date <- start_date <- end <- start <- ongoing <- NULL + + # unwrap recess_table from list + recess_table <- recess_table[[1]] + + # Filtering out ongoing recessions if specified + if (!show_ongoing) {recess_table <- dplyr::filter(recess_table, ongoing == F)} + + # use xformat to create correct "start" and "end" vars... + if (xformat == "date") { + # ... by renaming existing date fields (for date axis) + recessions <- dplyr::rename(recess_table, start = start_date, end = end_date) + } else { + # ... or by creating decimal dates (for numeric axis) + if (xformat != "numeric") { + warning("geom_recessions currently only supports x axes in the numeric and date formats. Using numeric.") + } + recessions <- dplyr::mutate( + recess_table, + start = lubridate::decimal_date(start_date), + end = lubridate::decimal_date(end_date) + ) + } + + # Remove recessions outside of range + recessions <- dplyr::filter(recessions, end > min & start < max) + + # If `min` or `max` fall in middle of a recession, modify recession to end at specified term. + recessions <- dplyr::transmute( + recessions, + start = if_else(start < min, min, as.numeric(start)), + end = if_else(end > max, max, as.numeric(end)), + ) + + return(recessions) +} + + +#' @name customproto +NULL + +#' @describeIn customproto Add recession bars to plot. +#' @format NULL +#' @usage NULL +#' @export +GeomRecessions <- ggproto( + "GeomRecessions", Geom, + default_aes = aes(colour = NA, alpha = 0.11, linewidth = 0.5, linetype = 1, na.rm = TRUE), + + required_aes = c("xformat", "ymin", "ymax", "show_ongoing", "recess_table" ,"fill"), + + # replace `data` with `recessions`, filtered by `data` + setup_data = function(data, params) { + #filter recessions based on date parameters from `data` and return it. This overwrites `data`. + data <- filter_recessions(min = min(data$x), max = max(data$x), + xformat = params$xformat, + show_ongoing = params$show_ongoing, + recess_table = params$recess_table) + + # set up data for GeomRect + data <- dplyr::transmute( + data, + xmin = start, + xmax = end, + ymin = params$ymin, + ymax = params$ymax, + PANEL = 1, + group = -1, + # re-establish dummy aesthetic, needed for legend entry + fill = "Recession" + ) + + return(data) + }, + + # remainder untouched from `geom_rect`: + draw_panel = function(self, data, panel_params, coord, linejoin = "mitre") { + + if (!coord$is_linear()) { + aesthetics <- setdiff( + names(data), c("x", "y", "xmin", "xmax", "ymin", "ymax") + ) + + polys <- lapply(split(data, seq_len(nrow(data))), function(row) { + poly <- rect_to_poly(row$xmin, row$xmax, row$ymin, row$ymax) + aes <- new_data_frame(row[aesthetics])[rep(1,5), ] + + GeomPolygon$draw_panel(cbind(poly, aes), panel_params, coord) + }) + + ggname("bar", do.call("grobTree", polys)) + } else { + coords <- coord$transform(data, panel_params) + ggname("geom_rect", rectGrob( + coords$xmin, coords$ymax, + width = coords$xmax - coords$xmin, + height = coords$ymax - coords$ymin, + default.units = "native", + just = c("left", "top"), + gp = gpar( + col = coords$colour, + fill = alpha(coords$fill, coords$alpha), + lwd = coords$linewidth * .pt, + lty = coords$linetype, + linejoin = linejoin, + # `lineend` is a workaround for Windows and intentionally kept unexposed + # as an argument. (c.f. https://github.com/tidyverse/ggplot2/issues/3037#issuecomment-457504667) + lineend = if (identical(linejoin, "round")) "round" else "square" + ) + )) + } + }, + + draw_key = draw_key_polygon +) + + +#' @describeIn customproto Add recession bar labels to plot. +#' @format NULL +#' @usage NULL +#' @export +GeomRecessionsText <- ggproto( + "GeomRecessionsText", Geom, + + required_aes = c("xformat", "label", "show_ongoing", "recess_table", "y"), + + default_aes = aes( + colour = "black", size = 3.88, alpha = NA, family = "", fontface = 1, lineheight = 1.2, + xformat = NULL, angle = 270, parse = FALSE, + check_overlap = FALSE, na.rm = TRUE, + hjust = "left", vjust = "bottom" + ), + + # replace `data` with `recessions`, filtered by `data` + setup_data = function(data, params) { + #filter recessions based on date parameters from `data` and return it. This overwrites `data`. + data <- filter_recessions(min = min(data$x), max = max(data$x), + xformat = params$xformat, + show_ongoing = params$show_ongoing, + recess_table = params$recess_table) + + # set up data for GeomRect + data <- dplyr::transmute( + data, + x = end, + y = params$y, + PANEL = 1, + group = -1 + ) + + return(data) + }, + + # remainder untouched from `geom_text`: + draw_panel = function(data, panel_params, coord, parse = FALSE, + na.rm = FALSE, check_overlap = FALSE) { + lab <- data$label + if (parse) { + lab <- parse_safe(as.character(lab)) + } + + data <- coord$transform(data, panel_params) + if (is.character(data$vjust)) { + data$vjust <- compute_just(data$vjust, data$y) + } + if (is.character(data$hjust)) { + data$hjust <- compute_just(data$hjust, data$x) + } + + textGrob( + lab, + data$x, data$y, default.units = "native", + hjust = data$hjust, vjust = data$vjust, + rot = data$angle, + gp = gpar( + col = alpha(data$colour, data$alpha), + fontsize = data$size * .pt, + fontfamily = data$family, + fontface = data$fontface, + lineheight = data$lineheight + ), + check.overlap = check_overlap + ) + }, + + draw_key = draw_key_text +) + + +#'Update recessions table +#' +#'The cmapplot package contains an internal dataset \code{recessions} of all +#'recessions in American history as recorded by the National Bureau of Economic +#'Research (NBER). However, users may need to replace the built-in data, such as +#'in the event of new recessions and/or changes to the NBER consensus on +#'recession dates. This function fetches and interprets this data from the NBER +#'website. +#' +#'@param url Char, the web location of the NBER machine-readable CSV file. The +#' default, \code{NULL}, uses the most recently identified URL known to the +#' package development team, which appears to be the most stable location for +#' updates over time. +#'@param quietly Logical, suppresses messages produced by +#' \code{utils::download.file}. +#' +#'@return A data frame with the following variables: \itemize{ \item +#' \code{start_char, end_char}: Chr. Easily readable labels for the beginning +#' and end of the recession. \item \code{start_date, end_date}: Date. Dates +#' expressed in R datetime format, using the first day of the specified month. +#' \item \code{ongoing}: Logical. Whether or not the recession is ongoing as of +#' the latest available NBER data. } +#' +#'@source \url{https://www.nber.org/data/cycles/cycle dates pasted.csv} +#' +#' @examples +#' recessions <- update_recessions() +#' +#' # package maintainers can update the internal dataset from within +#' # package by running the following code: +#' \dontrun{ +#' recessions <- update_recessions() +#' usethis::use_data(recessions, internal = TRUE, overwrite = TRUE) +#' } +#' +#'@export +update_recessions <- function(url = NULL, quietly = FALSE){ + + # Use default URL if user does not override + if (is_null(url) | missing(url)) { + url <- "http://data.nber.org/data/cycles/20210719_cycle_dates_pasted.csv" + } + + # locally bind variable names + start_char <- end_char <- start_date <- end_date <- ongoing <- index <- peak <- trough <- NULL + + return( + # attempt to download and format recessions table + tryCatch({ + recessions <- read.csv(url) %>% + # drop first row trough + dplyr::slice(-1) %>% + # convert peaks and troughs... + dplyr::mutate( + # ...to R dates + start_date = as.Date(peak), + end_date = as.Date(trough), + # ... and clean char strings + start_char = format(start_date, "%b %Y"), + end_char = format(end_date, "%b %Y")) %>% + # confirm ascending and create row number + dplyr::arrange(start_date) %>% + mutate(index = row_number()) %>% + mutate( + # Flag unfinished recessions + ongoing = case_when( + is.na(end_date) & index == max(.$index) ~ T, + TRUE ~ F), + # set ongoing recession to arbitrary future date + end_date = case_when( + ongoing ~ as.Date("2500-01-01"), + TRUE ~ end_date), + # mark ongoing recession in char field + end_char = case_when( + ongoing ~ "Ongoing", + TRUE ~ end_char) + ) %>% + # clean up + select(start_char, end_char, start_date, end_date, ongoing) + + if (!quietly) {message("Successfully fetched from NBER")} + + # Return recessions + recessions + }, + error = function(cond){ + if (!quietly) message("WARNING: Fetch or processing failed. `NULL` returned.") + return(NULL) + } + ) + ) +} diff --git a/R/geom_text_lastonly.R b/R/geom_text_lastonly.R index f3307bc3..0732cfa9 100644 --- a/R/geom_text_lastonly.R +++ b/R/geom_text_lastonly.R @@ -1,403 +1,403 @@ -#' Text (Last Only) -#' -#' Label only the last point(s) on a plot. \code{geom_text_lastonly()} can be -#' used instead of \code{ggplot2::geom_text()} when only the last point(s) -#' should be labeled. This is accomplished by identifying the maximum value of -#' \code{x} in \code{data} and applying a filter to omit records where \code{x} -#' is less than the maximum. -#' -#' Labels are placed by default to the right of the final point, and may be -#' partially cut off by the plot limits. There are two known ways to address -#' this: \enumerate{ \item Turn off panel clipping, e.g. with -#' \code{coord_cartesian(clip = "off")}. Substitute the correct coordinate -#' system for your plot--all have a \code{clip} argument available. Note that -#' this will allow all geoms in the plot to draw outside the panel area, which -#' may have unintended consequences. \item Manually expand the \code{x} scale, -#' e.g. with \code{scale_x_continuous(expand=expand_scale(mult=0.10))} or -#' \code{coord_cartesian(xlim = c(min, max))}. } -#' -#' Code was mostly copied from the source of \code{ggplot2::geom_text()} and -#' \code{ggplot2::geom_point()}. -#' -#' @inheritParams ggplot2::layer -#' @inheritParams ggplot2::geom_point -#' @param parse If \code{TRUE}, the labels will be parsed into expressions and -#' displayed as described in \code{?plotmath}. -#' @param nudge_x,nudge_y Horizontal and vertical adjustment to nudge labels by. -#' Useful for offsetting text from points, particularly on discrete scales. -#' Cannot be jointy specified with \code{position}. -#' @param position Position adjustment, either as a string, or the result of a -#' call to a position adjustment function. Cannot be jointy specified with -#' \code{nudge_x} or \code{nudge_y}. -#' @param check_overlap If \code{TRUE}, text that overlaps previous text in the -#' same layer will not be plotted. \code{check_overlap} happens at draw time -#' and in the order of the data. Therefore data should be arranged by the -#' label column before calling \code{geom_text_lastonly()}. -#' @param add_points If \code{TRUE}, points will be added to the plot (for the -#' labeled data only). Default size=2, color will match line color. -#' @param text_aes,point_aes Named list, additional aesthetics to send to the -#' text and point geoms, respectively. -#' @param ... Additional aesthetics to send to BOTH the point and text geoms. -#' Note that if \code{add_points = FALSE}, additional parameters can be passed -#' to the text geom here, rather than in \code{text_aes}, without breaking. -#' -#' @examples -#' df <- data.frame(year=2010:2020, value=runif(22), var=c(rep("A", 11), rep("B", 11))) -#' -#' # Without points, label formatting or x-axis expansion -#' ggplot(df, aes(x=year, y=value, color=var)) + -#' geom_line() + -#' labs(title="Random lines") + -#' scale_y_continuous("Percentage of absolutely nothing") + -#' scale_x_continuous("Year") + -#' geom_text_lastonly() -#' -#' # With points, label formatting and x-axis expansion -#' ggplot(df, aes(x=year, y=value, color=var, label=sprintf("%.1f%%", 100*value))) + -#' geom_line() + -#' labs(title="Random lines") + -#' scale_y_continuous("Percentage of absolutely nothing", labels=scales::percent) + -#' scale_x_continuous("Year", expand=expansion(mult=c(0.05, 0.10))) + -#' geom_text_lastonly(add_points=TRUE, text_aes=list(fontface="bold"), point_aes=list(size=2.5)) -#' -#' @export -geom_text_lastonly <- function( - mapping = NULL, - data = NULL, - stat = "identity", - position = NULL, - parse = FALSE, - nudge_x = 0.25, - nudge_y = 0, - check_overlap = FALSE, - na.rm = FALSE, - show.legend = FALSE, - inherit.aes = TRUE, - add_points = FALSE, - text_aes = NULL, - point_aes = NULL, - ... -) { - if (is.null(position)) { - position_lab <- position_nudge(nudge_x, nudge_y) - position_pt <- position_identity() - } - - elements <- list( - if (add_points) { - layer( - data = data, - mapping = mapping, - stat = stat, - geom = GeomPointLast, - position = position_pt, - show.legend = show.legend, - inherit.aes = inherit.aes, - params = append( - list( - na.rm = na.rm, - ... - ), - point_aes - ) - ) - }, - layer( - data = data, - mapping = mapping, - stat = stat, - geom = GeomTextLast, - position = position_lab, - show.legend = show.legend, - inherit.aes = inherit.aes, - params = append( - list( - parse = parse, - check_overlap = check_overlap, - na.rm = na.rm, - ... - ), - text_aes - ) - ) - ) - return(elements) -} - - -#' Custom ggproto classes -#' -#' The \code{cmapplot} package contains a few custom ggproto objects. For the -#' most part, these are slightly tweaked versions of ggplot2's default proto -#' objects. For more information about these, see -#' \code{\link[ggplot2:ggplot2-ggproto]{ggplot2::ggplot2-ggproto}}. -#' -#' @name customproto -NULL - -#' @describeIn customproto Add text to plot for maximum x-value in dataset only. -#' @format NULL -#' @usage NULL -#' @export -GeomTextLast <- ggproto( - "GeomTextLast", - Geom, - required_aes = c("x", "y"), - - default_aes = aes( - colour = "black", - size = 3.88, - angle = 0, - hjust = 0, - vjust = 0.5, - alpha = NA, - family = "", - fontface = 1, - lineheight = 1.2, - label = NA, - # Dummy parameters to match named params in GeomPointLast: - shape = NA, - fill = NA, - stroke = NA - ), - - draw_panel = function( - data, - panel_params, - coord, - parse = FALSE, - na.rm = FALSE, - check_overlap = FALSE - ) { - # Filter labeled dataset to include maximum x-value only - x_max <- max(unique(data$x)) - data <- data[data$x == x_max, ] - - # Use y-var as label if not otherwise specified - if (!is.na(data$label[[1]])) { - lab <- data$label - } else { - lab <- data$y - } - if (parse) { - lab <- parse_safe(as.character(lab)) - } - - data <- coord$transform(data, panel_params) - if (is.character(data$vjust)) { - data$vjust <- compute_just(data$vjust, data$y) - } - if (is.character(data$hjust)) { - data$hjust <- compute_just(data$hjust, data$x) - } - - textGrob( - lab, - data$x, - data$y, - default.units = "native", - hjust = data$hjust, - vjust = data$vjust, - rot = data$angle, - gp = gpar( - col = alpha(data$colour, data$alpha), - fontsize = data$size * .pt, - fontfamily = data$family, - fontface = data$fontface, - lineheight = data$lineheight - ), - check.overlap = check_overlap - ) - }, - - draw_key = draw_key_text -) - -# GeomTextLast helper functions -compute_just <- function(just, x) { - inward <- just == "inward" - just[inward] <- c("left", "middle", "right")[just_dir(x[inward])] - outward <- just == "outward" - just[outward] <- c("right", "middle", "left")[just_dir(x[outward])] - - unname(c( - left = 0, - center = 0.5, - right = 1, - bottom = 0, - middle = 0.5, - top = 1 - )[just]) -} - -just_dir <- function(x, tol = 0.001) { - out <- rep(2L, length(x)) - out[x < 0.5 - tol] <- 1L - out[x > 0.5 + tol] <- 3L - out -} - - -#' @describeIn customproto Add points to plot for maximum x-value in dataset only. -#' @format NULL -#' @usage NULL -#' @export -GeomPointLast <- ggproto( - "GeomPointLast", - Geom, - required_aes = c("x", "y"), - non_missing_aes = c("size", "shape", "colour"), - default_aes = aes( - shape = 19, - colour = "black", - size = 2.0, - fill = NA, - alpha = NA, - stroke = 0.5, - # Dummy parameters to match named params in GeomTextLast: - angle = NA, - hjust = NA, - vjust = NA, - family = NA, - fontface = NA, - lineheight = NA, - label = NA - ), - - draw_panel = function(data, panel_params, coord, na.rm = FALSE) { - # Filter labeled dataset to include maximum x-value only - x_max <- max(unique(data$x)) - data <- data[data$x == x_max, ] - - if (is.character(data$shape)) { - data$shape <- translate_shape_string(data$shape) - } - - coords <- coord$transform(data, panel_params) - ggname( - "geom_point", - pointsGrob( - coords$x, - coords$y, - pch = coords$shape, - gp = gpar( - col = alpha(coords$colour, coords$alpha), - fill = alpha(coords$fill, coords$alpha), - # Stroke is added around the outside of the point - fontsize = coords$size * .pt + coords$stroke * .stroke / 2, - lwd = coords$stroke * .stroke / 2 - ) - ) - ) - }, - - draw_key = draw_key_point -) - -# GeomPointLast helper functions -ggname <- function(prefix, grob) { - grob$name <- grobName(grob, prefix) - grob -} - -translate_shape_string <- function(shape_string) { - # strings of length 0 or 1 are interpreted as symbols by grid - if (nchar(shape_string[1]) <= 1) { - return(shape_string) - } - - pch_table <- c( - "square open" = 0, - "circle open" = 1, - "triangle open" = 2, - "plus" = 3, - "cross" = 4, - "diamond open" = 5, - "triangle down open" = 6, - "square cross" = 7, - "asterisk" = 8, - "diamond plus" = 9, - "circle plus" = 10, - "star" = 11, - "square plus" = 12, - "circle cross" = 13, - "square triangle" = 14, - "triangle square" = 14, - "square" = 15, - "circle small" = 16, - "triangle" = 17, - "diamond" = 18, - "circle" = 19, - "bullet" = 20, - "circle filled" = 21, - "square filled" = 22, - "diamond filled" = 23, - "triangle filled" = 24, - "triangle down filled" = 25 - ) - - shape_match <- charmatch(shape_string, names(pch_table)) - - invalid_strings <- is.na(shape_match) - nonunique_strings <- shape_match == 0 - - if (any(invalid_strings)) { - bad_string <- unique(shape_string[invalid_strings]) - n_bad <- length(bad_string) - - collapsed_names <- sprintf("\n* '%s'", bad_string[1:min(5, n_bad)]) - - more_problems <- if (n_bad > 5) { - sprintf( - "\n* ... and %d more problem%s", - n_bad - 5, - ifelse(n_bad > 6, "s", "") - ) - } else { - "" - } - - rlang::abort(glue::glue( - "Can't find shape name:", - collapsed_names, - more_problems - )) - } - - if (any(nonunique_strings)) { - bad_string <- unique(shape_string[nonunique_strings]) - n_bad <- length(bad_string) - - n_matches <- vapply( - bad_string[1:min(5, n_bad)], - function(shape_string) { - sum(grepl(paste0("^", shape_string), names(pch_table))) - }, - integer(1) - ) - - collapsed_names <- sprintf( - "\n* '%s' partially matches %d shape names", - bad_string[1:min(5, n_bad)], - n_matches - ) - - more_problems <- if (n_bad > 5) { - sprintf( - "\n* ... and %d more problem%s", - n_bad - 5, - ifelse(n_bad > 6, "s", "") - ) - } else { - "" - } - - rlang::abort(glue::glue( - "Shape names must be unambiguous:", - collapsed_names, - more_problems - )) - } - - unname(pch_table[shape_match]) -} +#' Text (Last Only) +#' +#' Label only the last point(s) on a plot. \code{geom_text_lastonly()} can be +#' used instead of \code{ggplot2::geom_text()} when only the last point(s) +#' should be labeled. This is accomplished by identifying the maximum value of +#' \code{x} in \code{data} and applying a filter to omit records where \code{x} +#' is less than the maximum. +#' +#' Labels are placed by default to the right of the final point, and may be +#' partially cut off by the plot limits. There are two known ways to address +#' this: \enumerate{ \item Turn off panel clipping, e.g. with +#' \code{coord_cartesian(clip = "off")}. Substitute the correct coordinate +#' system for your plot--all have a \code{clip} argument available. Note that +#' this will allow all geoms in the plot to draw outside the panel area, which +#' may have unintended consequences. \item Manually expand the \code{x} scale, +#' e.g. with \code{scale_x_continuous(expand=expand_scale(mult=0.10))} or +#' \code{coord_cartesian(xlim = c(min, max))}. } +#' +#' Code was mostly copied from the source of \code{ggplot2::geom_text()} and +#' \code{ggplot2::geom_point()}. +#' +#' @inheritParams ggplot2::layer +#' @inheritParams ggplot2::geom_point +#' @param parse If \code{TRUE}, the labels will be parsed into expressions and +#' displayed as described in \code{?plotmath}. +#' @param nudge_x,nudge_y Horizontal and vertical adjustment to nudge labels by. +#' Useful for offsetting text from points, particularly on discrete scales. +#' Cannot be jointy specified with \code{position}. +#' @param position Position adjustment, either as a string, or the result of a +#' call to a position adjustment function. Cannot be jointy specified with +#' \code{nudge_x} or \code{nudge_y}. +#' @param check_overlap If \code{TRUE}, text that overlaps previous text in the +#' same layer will not be plotted. \code{check_overlap} happens at draw time +#' and in the order of the data. Therefore data should be arranged by the +#' label column before calling \code{geom_text_lastonly()}. +#' @param add_points If \code{TRUE}, points will be added to the plot (for the +#' labeled data only). Default size=2, color will match line color. +#' @param text_aes,point_aes Named list, additional aesthetics to send to the +#' text and point geoms, respectively. +#' @param ... Additional aesthetics to send to BOTH the point and text geoms. +#' Note that if \code{add_points = FALSE}, additional parameters can be passed +#' to the text geom here, rather than in \code{text_aes}, without breaking. +#' +#' @examples +#' df <- data.frame(year=2010:2020, value=runif(22), var=c(rep("A", 11), rep("B", 11))) +#' +#' # Without points, label formatting or x-axis expansion +#' ggplot(df, aes(x=year, y=value, color=var)) + +#' geom_line() + +#' labs(title="Random lines") + +#' scale_y_continuous("Percentage of absolutely nothing") + +#' scale_x_continuous("Year") + +#' geom_text_lastonly() +#' +#' # With points, label formatting and x-axis expansion +#' ggplot(df, aes(x=year, y=value, color=var, label=sprintf("%.1f%%", 100*value))) + +#' geom_line() + +#' labs(title="Random lines") + +#' scale_y_continuous("Percentage of absolutely nothing", labels=scales::percent) + +#' scale_x_continuous("Year", expand=expansion(mult=c(0.05, 0.10))) + +#' geom_text_lastonly(add_points=TRUE, text_aes=list(fontface="bold"), point_aes=list(size=2.5)) +#' +#' @export +geom_text_lastonly <- function( + mapping = NULL, + data = NULL, + stat = "identity", + position = NULL, + parse = FALSE, + nudge_x = 0.25, + nudge_y = 0, + check_overlap = FALSE, + na.rm = FALSE, + show.legend = FALSE, + inherit.aes = TRUE, + add_points = FALSE, + text_aes = NULL, + point_aes = NULL, + ... +) { + if (is.null(position)) { + position_lab <- position_nudge(nudge_x, nudge_y) + position_pt <- position_identity() + } + + elements <- list( + if (add_points) { + layer( + data = data, + mapping = mapping, + stat = stat, + geom = GeomPointLast, + position = position_pt, + show.legend = show.legend, + inherit.aes = inherit.aes, + params = append( + list( + na.rm = na.rm, + ... + ), + point_aes + ) + ) + }, + layer( + data = data, + mapping = mapping, + stat = stat, + geom = GeomTextLast, + position = position_lab, + show.legend = show.legend, + inherit.aes = inherit.aes, + params = append( + list( + parse = parse, + check_overlap = check_overlap, + na.rm = na.rm, + ... + ), + text_aes + ) + ) + ) + return(elements) +} + + +#' Custom ggproto classes +#' +#' The \code{cmapplot} package contains a few custom ggproto objects. For the +#' most part, these are slightly tweaked versions of ggplot2's default proto +#' objects. For more information about these, see +#' \code{\link[ggplot2:ggplot2-ggproto]{ggplot2::ggplot2-ggproto}}. +#' +#' @name customproto +NULL + +#' @describeIn customproto Add text to plot for maximum x-value in dataset only. +#' @format NULL +#' @usage NULL +#' @export +GeomTextLast <- ggproto( + "GeomTextLast", + Geom, + required_aes = c("x", "y"), + + default_aes = aes( + colour = "black", + size = 3.88, + angle = 0, + hjust = 0, + vjust = 0.5, + alpha = NA, + family = "", + fontface = 1, + lineheight = 1.2, + label = NA, + # Dummy parameters to match named params in GeomPointLast: + shape = NA, + fill = NA, + stroke = NA + ), + + draw_panel = function( + data, + panel_params, + coord, + parse = FALSE, + na.rm = FALSE, + check_overlap = FALSE + ) { + # Filter labeled dataset to include maximum x-value only + x_max <- max(unique(data$x)) + data <- data[data$x == x_max, ] + + # Use y-var as label if not otherwise specified + if (!is.na(data$label[[1]])) { + lab <- data$label + } else { + lab <- data$y + } + if (parse) { + lab <- parse_safe(as.character(lab)) + } + + data <- coord$transform(data, panel_params) + if (is.character(data$vjust)) { + data$vjust <- compute_just(data$vjust, data$y) + } + if (is.character(data$hjust)) { + data$hjust <- compute_just(data$hjust, data$x) + } + + textGrob( + lab, + data$x, + data$y, + default.units = "native", + hjust = data$hjust, + vjust = data$vjust, + rot = data$angle, + gp = gpar( + col = alpha(data$colour, data$alpha), + fontsize = data$size * .pt, + fontfamily = data$family, + fontface = data$fontface, + lineheight = data$lineheight + ), + check.overlap = check_overlap + ) + }, + + draw_key = draw_key_text +) + +# GeomTextLast helper functions +compute_just <- function(just, x) { + inward <- just == "inward" + just[inward] <- c("left", "middle", "right")[just_dir(x[inward])] + outward <- just == "outward" + just[outward] <- c("right", "middle", "left")[just_dir(x[outward])] + + unname(c( + left = 0, + center = 0.5, + right = 1, + bottom = 0, + middle = 0.5, + top = 1 + )[just]) +} + +just_dir <- function(x, tol = 0.001) { + out <- rep(2L, length(x)) + out[x < 0.5 - tol] <- 1L + out[x > 0.5 + tol] <- 3L + out +} + + +#' @describeIn customproto Add points to plot for maximum x-value in dataset only. +#' @format NULL +#' @usage NULL +#' @export +GeomPointLast <- ggproto( + "GeomPointLast", + Geom, + required_aes = c("x", "y"), + non_missing_aes = c("size", "shape", "colour"), + default_aes = aes( + shape = 19, + colour = "black", + size = 2.0, + fill = NA, + alpha = NA, + stroke = 0.5, + # Dummy parameters to match named params in GeomTextLast: + angle = NA, + hjust = NA, + vjust = NA, + family = NA, + fontface = NA, + lineheight = NA, + label = NA + ), + + draw_panel = function(data, panel_params, coord, na.rm = FALSE) { + # Filter labeled dataset to include maximum x-value only + x_max <- max(unique(data$x)) + data <- data[data$x == x_max, ] + + if (is.character(data$shape)) { + data$shape <- translate_shape_string(data$shape) + } + + coords <- coord$transform(data, panel_params) + ggname( + "geom_point", + pointsGrob( + coords$x, + coords$y, + pch = coords$shape, + gp = gpar( + col = alpha(coords$colour, coords$alpha), + fill = alpha(coords$fill, coords$alpha), + # Stroke is added around the outside of the point + fontsize = coords$size * .pt + coords$stroke * .stroke / 2, + lwd = coords$stroke * .stroke / 2 + ) + ) + ) + }, + + draw_key = draw_key_point +) + +# GeomPointLast helper functions +ggname <- function(prefix, grob) { + grob$name <- grobName(grob, prefix) + grob +} + +translate_shape_string <- function(shape_string) { + # strings of length 0 or 1 are interpreted as symbols by grid + if (nchar(shape_string[1]) <= 1) { + return(shape_string) + } + + pch_table <- c( + "square open" = 0, + "circle open" = 1, + "triangle open" = 2, + "plus" = 3, + "cross" = 4, + "diamond open" = 5, + "triangle down open" = 6, + "square cross" = 7, + "asterisk" = 8, + "diamond plus" = 9, + "circle plus" = 10, + "star" = 11, + "square plus" = 12, + "circle cross" = 13, + "square triangle" = 14, + "triangle square" = 14, + "square" = 15, + "circle small" = 16, + "triangle" = 17, + "diamond" = 18, + "circle" = 19, + "bullet" = 20, + "circle filled" = 21, + "square filled" = 22, + "diamond filled" = 23, + "triangle filled" = 24, + "triangle down filled" = 25 + ) + + shape_match <- charmatch(shape_string, names(pch_table)) + + invalid_strings <- is.na(shape_match) + nonunique_strings <- shape_match == 0 + + if (any(invalid_strings)) { + bad_string <- unique(shape_string[invalid_strings]) + n_bad <- length(bad_string) + + collapsed_names <- sprintf("\n* '%s'", bad_string[1:min(5, n_bad)]) + + more_problems <- if (n_bad > 5) { + sprintf( + "\n* ... and %d more problem%s", + n_bad - 5, + ifelse(n_bad > 6, "s", "") + ) + } else { + "" + } + + rlang::abort(glue::glue( + "Can't find shape name:", + collapsed_names, + more_problems + )) + } + + if (any(nonunique_strings)) { + bad_string <- unique(shape_string[nonunique_strings]) + n_bad <- length(bad_string) + + n_matches <- vapply( + bad_string[1:min(5, n_bad)], + function(shape_string) { + sum(grepl(paste0("^", shape_string), names(pch_table))) + }, + integer(1) + ) + + collapsed_names <- sprintf( + "\n* '%s' partially matches %d shape names", + bad_string[1:min(5, n_bad)], + n_matches + ) + + more_problems <- if (n_bad > 5) { + sprintf( + "\n* ... and %d more problem%s", + n_bad - 5, + ifelse(n_bad > 6, "s", "") + ) + } else { + "" + } + + rlang::abort(glue::glue( + "Shape names must be unambiguous:", + collapsed_names, + more_problems + )) + } + + unname(pch_table[shape_match]) +} diff --git a/R/theme_cmap.R b/R/theme_cmap.R index e2c95b3f..8ab66e45 100644 --- a/R/theme_cmap.R +++ b/R/theme_cmap.R @@ -156,7 +156,7 @@ theme_cmap <- function( if(!is.null(hline)){ add_to_obj(ggplot2::geom_hline( yintercept = hline, - size = gg_lwd_convert(consts$lwd_strongline), + linewidth = gg_lwd_convert(consts$lwd_strongline), color = cmapplot_globals$colors$blackish)) } @@ -164,7 +164,7 @@ theme_cmap <- function( if(!is.null(vline)){ add_to_obj(ggplot2::geom_vline( xintercept = vline, - size = gg_lwd_convert(consts$lwd_strongline), + linewidth = gg_lwd_convert(consts$lwd_strongline), color = cmapplot_globals$colors$blackish)) } @@ -184,35 +184,35 @@ theme_cmap <- function( # horizontal gridlines, if specified if (grepl("h", gridlines)) { attr[["panel.grid.major.y"]] <- ggplot2::element_line( - size = gg_lwd_convert(consts$lwd_gridline), + linewidth = gg_lwd_convert(consts$lwd_gridline), color = cmapplot_globals$colors$blackish) } # vertical gridlines, if specified if (grepl("v", gridlines)) { attr[["panel.grid.major.x"]] <- ggplot2::element_line( - size = gg_lwd_convert(consts$lwd_gridline), + linewidth = gg_lwd_convert(consts$lwd_gridline), color = cmapplot_globals$colors$blackish) } # x axis line, if specified if (grepl("x", axislines)) { attr[["axis.line.x"]] <- ggplot2::element_line( - size = gg_lwd_convert(consts$lwd_gridline), + linewidth = gg_lwd_convert(consts$lwd_gridline), color = cmapplot_globals$colors$blackish) } # y axis line, if specified if (grepl("y", axislines)) { attr[["axis.line.y"]] <- ggplot2::element_line( - size = gg_lwd_convert(consts$lwd_gridline), + linewidth = gg_lwd_convert(consts$lwd_gridline), color = cmapplot_globals$colors$blackish) } # x axis ticks, if specified if (grepl("x", axisticks)) { attr[["axis.ticks.x"]] <- ggplot2::element_line( - size = gg_lwd_convert(consts$lwd_gridline), + linewidth = gg_lwd_convert(consts$lwd_gridline), color = cmapplot_globals$colors$blackish) attr[["axis.ticks.length.x"]] <- unit(consts$length_ticks,"bigpts") @@ -221,7 +221,7 @@ theme_cmap <- function( # y axis ticks, if specified if (grepl("y", axisticks)) { attr[["axis.ticks.y"]] <- ggplot2::element_line( - size = gg_lwd_convert(consts$lwd_gridline), + linewidth = gg_lwd_convert(consts$lwd_gridline), color = cmapplot_globals$colors$blackish) attr[["axis.ticks.length.y"]] <- unit(consts$length_ticks,"bigpts") @@ -262,18 +262,18 @@ theme_cmap_base <- function(consts = cmapplot_globals$consts, # building blocks line = element_line( colour = cmapplot_globals$colors$blackish, - size = gg_lwd_convert(consts$lwd_gridline), + linewidth = gg_lwd_convert(consts$lwd_gridline), linetype = 1, lineend = "butt", inherit.blank = TRUE), rect = element_rect( fill = NA, colour = ifelse(debug, "blue", NA), - size = 0.5, linetype = 1, + linewidth = 0.5, linetype = 1, inherit.blank = TRUE), text = element_text( family = cmapplot_globals$font$regular$family, - face = cmapplot_globals$fgiont$regular$face, + face = cmapplot_globals$font$regular$face, size = cmapplot_globals$fsize$M, color = cmapplot_globals$colors$blackish, lineheight = 0.9, hjust = 0.5, vjust = 0.5, angle = 0, diff --git a/cmapplot.Rproj b/cmapplot.Rproj index b51c8bca..e9b9fdd9 100644 --- a/cmapplot.Rproj +++ b/cmapplot.Rproj @@ -15,7 +15,7 @@ LaTeX: pdfLaTeX AutoAppendNewline: Yes StripTrailingWhitespace: Yes -LineEndingConversion: Windows +LineEndingConversion: Posix BuildType: Package PackageUseDevtools: Yes diff --git a/pkgdown/logo/cmap_logo.svg b/pkgdown/logo/cmap_logo.svg index fe79fde6..bcf99344 100644 --- a/pkgdown/logo/cmap_logo.svg +++ b/pkgdown/logo/cmap_logo.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + \ No newline at end of file