R/factory.R

Defines functions factory

#' Factory to create tables in different output formats using standardized
#' inputs.
#'
#' @param tab table body (data.frame)
#' @param hrule position of horizontal rules (integer vector)
#' @noRd
#' @inheritParams datasummary
#' @inheritParams modelsummary
factory <- function(tab,
                    align = NULL,
                    fmt = 3,
                    hrule = NULL,
                    notes = NULL,
                    output = NULL,
                    title = NULL,
                    add_rows = NULL,
                    add_columns = NULL,
                    escape = TRUE,
                    ...) {


  # sanity check functions are hosted in R/sanity_checks.R
  sanity_title(title)
  sanity_notes(notes)
  # sanitize_output(output) # do not override (otherwise this breaks quarto for shape="rbind")

  # parse output
  if (settings_equal("output_factory", "gt")) {
    factory_fun <- factory_gt
  } else if (settings_equal("output_factory", "kableExtra")) {
    factory_fun <- factory_kableExtra
  } else if (settings_equal("output_factory", "flextable")) {
    factory_fun <- factory_flextable
  } else if (settings_equal("output_factory", "huxtable")) {
    factory_fun <- factory_huxtable
  } else if (settings_equal("output_factory", "DT")) {
    factory_fun <- factory_DT
  } else if (settings_equal("output_factory", "dataframe")) {
    factory_fun <- factory_dataframe
  } else if (settings_equal("output_factory", "modelsummary")) {
    factory_fun <- factory_markdown
  } else if (settings_equal("output_factory", "typst")) {
    factory_fun <- factory_typst
  }

  # flat header if necessary
  flat_header <- attr(tab, 'header_sparse_flat')
  if (!is.null(flat_header)) {
    flat_factories <- c('flextable', 'huxtable', 'dataframe', 'typst')
    flat_formats <- c('markdown', 'word', 'powerpoint', 'typst')
    if (settings_get("output_factory") %in% flat_factories ||
        settings_get("output_format") %in% flat_formats) {
        attr(tab, "header_bottom") <- colnames(tab)

      # datasummary_balance with dinm produces more cols than flat_header
      for (i in seq_along(flat_header)) {
        colnames(tab)[i] <- flat_header[i]
      }

    }
  }

  # de-duplicate columns with whitespace
  colnames(tab) <- pad(colnames(tab))

  # add_columns
  if (!is.null(add_columns)) {

    # sanity check
    checkmate::assert_data_frame(add_columns, min.cols = 1, min.rows = 1)

    pos <- attr(add_columns, 'position')

    # `fmt`: modelsummary() supplies a list with `fmt` default, but not other functions
    if (isTRUE(checkmate::check_list(fmt))) {
      fmt <- fmt[["fmt"]]
    }

    fun <- sanitize_fmt(fmt)
    for (i in seq_along(add_columns)) {
      if (is.numeric(add_columns[[i]])) {
        add_columns[[i]] <- fun(add_columns[[i]])
      } else {
        add_columns[[i]] <- as.character(add_columns[[i]])
      }
    }

    # pad with empty cells if insufficient rows
    nrow_gap <- nrow(tab) - nrow(add_columns)
    if (nrow_gap > 0) {
      tmp <- matrix('', ncol = ncol(add_columns), nrow = nrow_gap)
      tmp <- data.frame(tmp)
      colnames(tmp) <- colnames(add_columns)
      add_columns <- bind_rows(add_columns, tmp)
    }

    # append
    for (i in seq_along(add_columns)) {
      if (!is.null(pos) && !is.na(pos[i])) {
        lef <- tab[, -c(pos[i]:ncol(tab)), drop = FALSE]
        rig <- tab[, c(pos[i]:ncol(tab)), drop = FALSE]
        tab <- bind_cols(lef, add_columns[i], rig)
      } else {
        tab <- bind_cols(tab, add_columns[i])
      }
    }

    # pad headers
    ks <- attr(tab, 'span_kableExtra')
    if (!is.null(ks)) {
      for (i in seq_along(ks)) {
        # 5 spaces is a hack
        ks[[i]] <- c(ks[[i]], '     ' = ncol(add_columns))
      }
      attr(tab, 'span_kableExtra') <- ks
    }
  }

  # add_rows
  if (!is.null(add_rows)) {

    # data.frame includes metadata columns
    if (settings_equal("output_format", "dataframe")) {
      # only for modelsummary, not for datasummary

      if (all(c("term", "statistic") %in% colnames(tab))) {
        add_rows$part <- "manual"
        add_rows$statistic <- ""
        add_rows <- add_rows[, colnames(tab)]
      }

    }

    # sanity check
    checkmate::assert_data_frame(add_rows, min.rows = 1, ncols = ncol(tab))

    colnames(add_rows) <- colnames(tab)
    pos <- attr(add_rows, 'position')

    # convert to character
    fun <- sanitize_fmt(fmt)
    for (i in 1:ncol(add_rows)) {
      if (is.numeric(add_rows[[i]])) {
        add_rows[[i]] <- fun(add_rows[[i]])
      } else {
        add_rows[[i]] <- as.character(add_rows[[i]])
      }
    }

    # append
    for (i in 1:nrow(add_rows)) {
      # append
      if (!is.null(pos) && !is.na(pos[i])) {
        top <- tab[-c(pos[i]:nrow(tab)), , drop = FALSE]
        bot <- tab[c(pos[i]:nrow(tab)), , drop = FALSE]
        tab <- bind_rows(top, add_rows[i, , drop = FALSE], bot)
      } else {
        tab <- bind_rows(tab, add_rows[i, , drop = FALSE])
      }
    }
  }

  ## align: sanity must be checked after add_columns
  if (is.null(align)) {
    align <- strrep("l", ncol(tab))
  } else {
    checkmate::assert_true(nchar(align) == ncol(tab))
  }
  align <- strsplit(align, "")[[1]]

  # dot align with unicode spaces (latex has its own mechanism)
  if (!settings_equal("output_format", "latex")) {
    align_d <- grep("d", align)
    for (i in align_d) {
      tab[[i]] <- pad(tab[[i]], style = "character")
    }
    align[align == "d"] <- "c"
  }

  ## build table
  out <- factory_fun(tab,
    align = align,
    hrule = hrule,
    notes = notes,
    title = title,
    escape = escape,
    ...)


  if (output == "jupyter" ||
      (output == "default" && settings_equal("output_default", "jupyter"))) {
    insight::check_if_installed("IRdisplay")
    IRdisplay::display_html(as.character(out))
  } else {
    return(out)
  }
}

Try the modelsummary package in your browser

Any scripts or data that you put into this service are public.

modelsummary documentation built on Oct. 15, 2023, 5:06 p.m.