R/theme_schneidr.R

Defines functions theme_schneidr

Documented in theme_schneidr

#' A simple, reader-friendly ggplot2 theme
#'
#' @description A simple ggplot2 theme that tries to simplify the reader's experience by reducing unnecessary lines, and aligning text to be immediately readable. Readers shouldn't have to tilt their heads to read axis labels, and the titles should be the first thing they read.
#'
#' @param base_size A single number giving the base size of text in the plot.
#' @param base_font_family Name of a font family to be used in all text outside of overall plot titles.
#' @param titles_font_family Name of a font family to be used in overall plot titles.
#' @param grid_lines A single value from 'x', 'y', `TRUE`, or `FALSE`, specifying which dimensions of gridlines should be included.
#' @param axis_lines A single value from 'x', 'y', `TRUE`, or `FALSE`, specifying which axes should be marked with a line.
#' @param axis_ticks A single value from 'x', 'y', `TRUE`, or `FALSE`, specifying which axes should have tick marks on breaks.
#' @param x_axis_text_align A single value from 'left', 'center', or 'right', indicating how x axis labels should be aligned.
#' @param y_axis_text_align A single value from 'left', 'center', or 'right', indicating how y axis labels should be aligned.
#' @param markdown_elements Specify which plot elements should be created using `ggtext::element_markdown()`, allowing the use of markdown text including CSS and HTML. Can be ither 'none' or a character vector with one or more of the following: 'title', 'subtitle', 'caption', 'axis title', 'axis label', or 'strip'.
#'
#' @return A full theme element.
#' @export
#'
#' @examples
#'
#' library(ggplot2)
#' mtcars[['gear']] <- factor(mtcars[['gear']])
#' mtcars_plot <- ggplot(mtcars) +
#' geom_point(aes(x = wt, y = mpg, color = gear)) +
#' labs(title = "Fuel economy and weight",
#'      subtitle = "Data from the 1974 Motor Trend US magazine.",
#'      x = "Weight (1000 lbs)",
#'      y = "Fuel economy (mpg)")
#'
#' # Basic theme example
#'   mtcars_plot +
#'     theme_schneidr(grid_lines = TRUE, axis_lines = FALSE)
#'
#' # Using markdown elements
#'
#'  mtcars_plot +
#'    labs(title = "Fuel economy and weight",
#'         subtitle = "Data from the 1974 Motor Trend US magazine.",
#'         x = "**Weight**<br>*1000 lbs*",
#'         y = "**Fuel economy**<br>*mpg*")
#'    theme_schneidr(grid_lines = TRUE, axis_lines = FALSE,
#'                   markdown_elements = 'axis title')

theme_schneidr <- function(base_size = 8,
                           base_font_family = "Rubik",
                           titles_font_family = "Libre Franklin",
                           grid_lines = FALSE,
                           axis_lines = TRUE,
                           axis_ticks = TRUE,
                           x_axis_text_align = 'center',
                           y_axis_text_align = 'center',
                           markdown_elements = 'none') {

  # Set appropriate ggplot2 element function to use depending on whether an element is markdown
    if ("ggtext" %in% .packages(all.available = TRUE)) {
      if (is.null(markdown_elements) || markdown_elements == 'none') {
        title_text_element <- ggplot2::element_text
        subtitle_text_element <- ggplot2::element_text
        caption_text_element <- ggplot2::element_text
        axis_title_text_element <- ggplot2::element_text
        axis_label_text_element <- ggplot2::element_text
        strip_text_element <- ggplot2::element_text
      } else {
        if ('title' %in% markdown_elements) {
          title_text_element <- ggtext::element_markdown
        } else {title_text_element <- ggplot2::element_text}
        if ('subtitle' %in% markdown_elements) {
          subtitle_text_element <- ggtext::element_markdown
        } else {subtitle_text_element <- ggplot2::element_text}
        if ('caption' %in% markdown_elements) {
          caption_text_element <- ggtext::element_markdown
        } else {caption_text_element <- ggplot2::element_text}
        if ('axis title' %in% markdown_elements) {
          axis_title_text_element <- ggtext::element_markdown
        } else {axis_title_text_element <- ggplot2::element_text}
        if ('axis label' %in% markdown_elements) {
          axis_label_text_element <- ggtext::element_markdown
        } else {axis_label_text_element <- ggplot2::element_text}
        if ('strip' %in% markdown_elements) {
          strip_text_element <- ggtext::element_markdown
        } else {strip_text_element <- ggplot2::element_text}
      }
    } else {
      stop("The `ggtext` package must be installed in order to use markdown elements.")
    }


  # Error check and message for numeric arguments
    if (!is.numeric(base_size)) {
      stop("The `base_size` argument must be specified as a single number.")
    }


  # Check if the specified font family can be loaded, potentially by relying on the `extrafont` R package
    available_fonts <- names(windowsFonts())

    if (!base_font_family %in% available_fonts) {
      if ('extrafont' %in% .packages(all.available = TRUE)) {

        if (base_font_family %in% extrafont::fonts()) {

          library('extrafont')
          extrafont::loadfonts(device = 'win', quiet = TRUE)

          warning(paste0('Fonts have been loaded via the `extrafont` package in order to use ', base_font_family))

        } else {

          rlang::warn(paste("The family", base_font_family, "is not available; using 'sans' instead.",
                            "Make sure the font is installed on this machine. If it's not one of R's default fonts, try registering and loading it with the `extrafont` package."),
                      .frequency = "once", .frequency_id = "base_font_family_warning")
          base_font_family <- 'sans'
        }
      }
    }

    if (!titles_font_family %in% available_fonts) {
      if ('extrafont' %in% .packages(all.available = TRUE)) {

        if (titles_font_family %in% extrafont::fonts()) {

          library('extrafont')
          extrafont::loadfonts(device = 'win', quiet = TRUE)

          warning(paste0('Fonts have been loaded via the `extrafont` package in order to use ', titles_font_family))

        } else {

          rlang::warn(paste("The family", titles_font_family, "is not available; using 'sans' instead.",
                            "Make sure the font is installed on this machine. If it's not one of R's default fonts, try registering and loading it with the `extrafont` package."),
                      .frequency = "once", .frequency_id = "titles_font_family_warning")
          titles_font_family <- 'sans'
        }
      }
    }

    # Handling of axis lines
      if (!is.null(axis_lines) && !(is.logical(axis_lines) || tolower(axis_lines) %in% c('x', 'y', 'xy', 'yx'))) {
        stop("If `axis_lines` is specified, it must be either `TRUE`, `FALSE`, 'x', 'y', 'xy', or 'yx'.")
      }

      if (is.null(axis_lines) || (is.logical(axis_lines) && !axis_lines)) {

        use_x_axis_lines <- FALSE
        use_y_axis_lines <- FALSE

      } else {

        axis_lines_element <- ggplot2::element_line(colour = 'lightgrey')

        if (is.character(axis_lines)) {
          use_x_axis_lines <- grepl(pattern = 'x', x = axis_lines, ignore.case = TRUE)
          use_y_axis_lines <- grepl(pattern = 'y', x = axis_lines, ignore.case = TRUE)
        }

        if (is.logical(axis_lines) && axis_lines) {
          use_x_axis_lines <- TRUE
          use_y_axis_lines <- TRUE
        }
      }

      if (use_x_axis_lines) {
        axis_lines_x <- axis_lines_element
      } else {
        axis_lines_x <- ggplot2::element_blank()
      }

      if (use_y_axis_lines) {
        axis_lines_y <- axis_lines_element
      } else {
        axis_lines_y <- ggplot2::element_blank()
      }

    # Handling of axis ticks
      if (!is.null(axis_ticks) && !(is.logical(axis_ticks) || tolower(axis_ticks) %in% c('x', 'y', 'xy', 'yx'))) {
        stop("If `axis_ticks` is specified, it must be either `TRUE`, `FALSE`, 'x', 'y', 'xy', or 'yx'.")
      }

      if (is.null(axis_ticks) || (is.logical(axis_ticks) && !axis_ticks)) {

        use_x_axis_ticks <- FALSE
        use_y_axis_ticks <- FALSE

      } else {

        axis_ticks_element <- ggplot2::element_line(colour = 'lightgrey')

        if (is.character(axis_ticks)) {
          use_x_axis_ticks <- grepl(pattern = 'x', x = axis_ticks, ignore.case = TRUE)
          use_y_axis_ticks <- grepl(pattern = 'y', x = axis_ticks, ignore.case = TRUE)
        }

        if (is.logical(axis_ticks) && axis_ticks) {
          use_x_axis_ticks <- TRUE
          use_y_axis_ticks <- TRUE
        }
      }

      if (use_x_axis_ticks) {
        axis_ticks_x <- axis_ticks_element
      } else {
        axis_ticks_x <- ggplot2::element_blank()
      }

      if (use_y_axis_ticks) {
        axis_ticks_y <- axis_ticks_element
      } else {
        axis_ticks_y <- ggplot2::element_blank()
      }

  # Handling of grid lines
    if (!is.null(grid_lines) && !(is.logical(grid_lines) || tolower(grid_lines) %in% c('x', 'y', 'xy', 'yx'))) {
      stop("If `gridlines` is specified, it must be either `TRUE`, `FALSE`, 'x', 'y', 'xy', or 'yx'.")
    }

    if (is.null(grid_lines) || (is.logical(grid_lines) && !grid_lines)) {

      use_x_gridlines <- FALSE
      use_y_gridlines <- FALSE

    } else {

        major_gridlines <- ggplot2::element_line(colour = 'lightgrey')
        minor_gridlines <- ggplot2::element_blank()

        if (is.character(grid_lines)) {
          use_x_gridlines <- grepl(pattern = 'x', x = grid_lines, ignore.case = TRUE)
          use_y_gridlines <- grepl(pattern = 'y', x = grid_lines, ignore.case = TRUE)
        }

        if (is.logical(grid_lines) && grid_lines) {
          use_x_gridlines <- TRUE
          use_y_gridlines <- TRUE
        }
    }

    if (use_x_gridlines) {
      major_gridlines_x <- major_gridlines
      minor_gridlines_x <- minor_gridlines
    } else {
      major_gridlines_x <- ggplot2::element_blank()
      minor_gridlines_x <- ggplot2::element_blank()
    }

    if (use_y_gridlines) {
      major_gridlines_y <- major_gridlines
      minor_gridlines_y <- minor_gridlines
    } else {
      major_gridlines_y <- ggplot2::element_blank()
      minor_gridlines_y <- ggplot2::element_blank()
    }

  # Handling of text alignment for axes

    if (!is.null(x_axis_text_align)) {

      if (!x_axis_text_align %in% c("left", 'center', 'right')) {
        stop("`x_axis_text_align` must be one of: 'left', 'center', or 'right'")
      }
      x_axis_text_hjust <- switch (x_axis_text_align,
                                   'left' = 0,
                                   'right' = 1,
                                   'center' = 0.5
      )
    } else {
      x_axis_text_hjust <- 0.5
    }

    if (!is.null(y_axis_text_align)) {

      if (!y_axis_text_align %in% c("left", 'center', 'right')) {
        stop("`y_axis_text_align` must be one of: 'left', 'center', or 'right'")
      }
      y_axis_text_hjust <- switch (y_axis_text_align,
                                   'left' = 0,
                                   'right' = 1,
                                   'center' = 0.5
      )
    } else {
      y_axis_text_hjust <- 0.5
    }

  # Uniform line size based on the specified value of `base_size`
    scaled_line_size <- (base_size/10)*(grid::get.gpar('lineheight')$lineheight)

  # Combine all the specified custom elements into a list
  # which will be built into a theme
    custom_theme_elements <- structure(list(# Set text preference for plot titles, subtitles, and captions
                                            text = ggplot2::element_text(family = base_font_family,
                                                                         size = base_size, face = "plain", color = "#545454",
                                                                         hjust = 0.5, vjust = 0.5, angle = 0, lineheight = 1,
                                                                         margin = grid::unit(c(0, 0, 0, 0), 'pt'),
                                                                         debug = FALSE, inherit.blank = TRUE),
                                            plot.title = title_text_element(size = (1.4)*base_size, face = 'bold',
                                                                               color = "#545454",
                                                                               hjust = 0, vjust = 1, family = titles_font_family,
                                                                               margin = ggplot2::margin(b = base_size,
                                                                                                        unit = "pt")),
                                            plot.subtitle = subtitle_text_element(size = base_size,
                                                                                  face = "italic", family = titles_font_family,
                                                                                  hjust = 0, vjust = 2,
                                                                                  color= "#545454",
                                                                                  margin = ggplot2::margin(b = base_size,
                                                                                                           unit = "pt")),
                                            plot.caption = subtitle_text_element(size = 0.8*base_size,
                                                                                 face = "italic", family = titles_font_family,
                                                                                 hjust = 0, vjust = 2,
                                                                                 color= "#545454",
                                                                                 margin = ggplot2::margin(b = 0.5*base_size,
                                                                                                          unit = "pt")),
                                            # Position title/subtitle relative to entire plot (not to main panel)
                                            plot.title.position = 'plot', plot.caption.position = 'plot',
                                            # Plot background and grids
                                            panel.background = ggplot2::element_blank(), panel.border = ggplot2::element_blank(),
                                            panel.grid.major.x = major_gridlines_x,
                                            panel.grid.minor.x = minor_gridlines_x,
                                            panel.grid.major.y = major_gridlines_y,
                                            panel.grid.minor.y = minor_gridlines_y,
                                            # Plot legend
                                            legend.position = "top",
                                            legend.justification = c(0, 0),
                                            legend.text = ggplot2::element_text(size = (0.8)*base_size, colour = "#545454",
                                                                                family = base_font_family),
                                            legend.background = ggplot2::element_blank(),
                                            legend.key = ggplot2::element_rect(size = 0.5, colour = NA),
                                            # Axis lines and ticks
                                            axis.line.x = axis_lines_x,
                                            axis.line.y = axis_lines_y,
                                            axis.ticks.x = axis_ticks_x,
                                            axis.ticks.y = axis_ticks_y,
                                            # Axis text
                                            axis.text.x = axis_label_text_element(size = base_size, family = base_font_family,
                                                                                  colour = "#545454", vjust = 0.5, hjust = x_axis_text_hjust),
                                            axis.text.y = axis_label_text_element(size = base_size, colour = "#545454",
                                                                                  family = base_font_family, hjust = y_axis_text_hjust),
                                            axis.title.x.bottom = axis_title_text_element(size = base_size*(1.1), colour = "#545454",
                                                                                          face = "plain", family = base_font_family, angle = 0,
                                                                                          margin = ggplot2::margin(t = (.8)*base_size, b = (.8)*base_size,
                                                                                                                   l = 0, r = 0,
                                                                                                                   unit = "pt")),
                                            axis.title.y.left = axis_title_text_element(size = base_size*(1.1), colour = "#545454",
                                                                                        face = "plain", family = base_font_family, angle = 0,
                                                                                        vjust = 0.5,
                                                                                        margin = ggplot2::margin(t = (.8)*base_size, b = (.8)*base_size,
                                                                                                                 l = 0, r = (.8)*base_size,
                                                                                                                 unit = "pt")),
                                            axis.title.x.top = axis_title_text_element(size = base_size*(1.1), colour = "#545454",
                                                                                       face = "plain", family = base_font_family, angle = 0,
                                                                                       margin = ggplot2::margin(t = (.8)*base_size, b = (.8)*base_size,
                                                                                                                l = 0, r = 0,
                                                                                                                unit = "pt")),
                                            axis.title.y.right = axis_title_text_element(size = base_size*(1.1), colour = "#545454",
                                                                                         face = "plain", family = base_font_family, angle = 0,
                                                                                         vjust = 0.5,
                                                                                         margin = ggplot2::margin(t = (.8)*base_size, b = (.8)*base_size,
                                                                                                                  l = 0, r = (.8)*base_size,
                                                                                                                  unit = "pt")),
                                            # Strips used in facet labels
                                            strip.placement = "outside",
                                            strip.text.x = strip_text_element(size = base_size*(1.1), colour = "#545454",
                                                                                 face = "bold", family = base_font_family,
                                                                                 margin = ggplot2::margin(t = 0, b = (.8)*base_size,
                                                                                                          l = 0, r = 0,
                                                                                                          unit = "pt")),
                                            strip.text.y = strip_text_element(size = base_size*(1.1), colour = "#545454",
                                                                              face = "bold", family = base_font_family,
                                                                              margin = ggplot2::margin(t = 0, b = (.8)*base_size,
                                                                                                       l = 0, r = 0,
                                                                                                       unit = "pt")),
                                            strip.text.y.left = strip_text_element(size = base_size*(1.1), colour = "#545454",
                                                                                   face = "bold", family = base_font_family,
                                                                                   margin = ggplot2::margin(t = 0, b = (.8)*base_size,
                                                                                                            l = 0, r = 0,
                                                                                                            unit = "pt"),
                                                                                   angle = 0),
                                            strip.background = ggplot2::element_blank(),
                                            # Spacing between panels in faceted plots
                                            panel.spacing = grid::unit(scaled_line_size, "lines"),
                                            # Margins surrounding entire plot
                                            plot.margin = grid::unit(c('t' = 0.5, 'r' = 1.5,
                                                                       'b' = 0.5, 'l' = 0.5)*scaled_line_size,
                                                                     units = "lines")),
                                       class = "theme", complete = TRUE)

  # Update the minimalistic ggplot2 theme ("theme_minimal")
  # with the list of custom theme elements
    minimal_theme <- ggplot2::theme_minimal(base_family = base_font_family,
                                            base_size = base_size)

    result_theme <-  ggplot2::`%+replace%`(minimal_theme,
                                           custom_theme_elements)

  # Return the result
    return(result_theme)
}
bschneidr/schneidr documentation built on Dec. 25, 2021, 4:55 p.m.