R/nice_table.R

Defines functions prepare_report prepare_broom parse_formatter format_flex format_CI nice_table

Documented in nice_table

#' @title Easily make nice APA tables
#'
#' @description Make nice APA tables easily through a wrapper
#' around the `flextable` package with sensical defaults and
#' automatic formatting features.
#'
#' @details The resulting `flextable` objects can be opened in
#' Word with `print(table, preview ="docx")`, or saved to
#' Word with the `flextable::save_as_docx()` function.
#'
#' @param data The data frame, to be converted to a flextable.
#' The data frame cannot have duplicate column names.
#' @param italics Which columns headers should be italic? Useful
#' for column names that should be italic but that are not picked
#' up automatically by the function. Select with numerical range, e.g., 1:3.
#' @param highlight Highlight rows with statistically significant
#' results? Requires a column named "p" containing p-values.
#' Can either accept logical (TRUE/FALSE) OR a numeric value for
#' a custom critical p-value threshold (e.g., 0.10 or 0.001).
#' @param stars Logical. Whether to add asterisks for significant p values.
#' @param col.format.p Applies p-value formatting to columns
#' that cannot be named "p" (for example for a data frame full
#' of p-values, also because it is not possible to have more
#' than one column named "p"). Select with numerical range, e.g., 1:3.
#' @param col.format.r Applies r-value formatting to columns
#' that cannot be named "r" (for example for a data frame full
#' of r-values, also because it is not possible to have more
#' than one column named "r"). Select with numerical range, e.g., 1:3.
#' @param col.format.ci Applies 95% confidence interval formatting
#' to selected columns (e.g., when reporting more than one interval).
#' @param format.custom Applies custom formatting to columns
#' selected via the `col.format.custom` argument. This is useful
#' if one wants custom formatting other than for p- or r-values.
#' It can also be used to transform (e.g., multiply) certain values
#' or print a specific symbol along the values for instance.
#' @param col.format.custom Which columns to apply the custom
#' function to. Select with numerical range, e.g., 1:3.
#' @param width Width of the table, in percentage of the
#' total width, when exported e.g., to Word. For full width,
#' use `width = 1`.
#' @param spacing Spacing of the rows (1 = single space, 2 = double space)
#' @param broom If providing a tidy table produced with the
#' `broom` package, which model type to use if one wants
#' automatic formatting (options are "t.test", "lm", "cor.test",
#' and "wilcox.test").
#' @param report If providing an object produced with the
#' `report` package, which model type to use if one wants
#' automatic formatting (options are "t.test", "lm", and "cor.test").
#' @param short Logical. Whether to return an abbreviated
#' version of the tables made by the `report` package.
#' @param title Optional, to add a table header, if desired.
#' @param note Optional, to add one or more table footnote (APA note),
#' if desired.
#' @param separate.header Logical, whether to separate headers based
#' on name delimiters (i.e., periods ".").
#'
#' @keywords APA style table
#' @return An APA-formatted table of class "flextable"
#' @examplesIf requireNamespace("flextable", quietly = TRUE) && requireNamespace("methods", quietly = TRUE)
#' # Make the basic table
#' my_table <- nice_table(
#'   mtcars[1:3, ],
#'   title = c("Table 1", "Motor Trend Car Road Tests"),
#'   note = c(
#'     "The data was extracted from the 1974 Motor Trend US magazine.",
#'     "* p < .05, ** p < .01, *** p < .001"
#'   )
#' )
#' my_table
#'
#' \donttest{
#' # Save table to word
#' mypath <- tempfile(fileext = ".docx")
#' flextable::save_as_docx(my_table, path = mypath)
#' }
#'
#' # Publication-ready tables
#' mtcars.std <- lapply(mtcars, scale)
#' model <- lm(mpg ~ cyl + wt * hp, mtcars.std)
#' stats.table <- as.data.frame(summary(model)$coefficients)
#' CI <- confint(model)
#' stats.table <- cbind(
#'   row.names(stats.table),
#'   stats.table, CI
#' )
#' names(stats.table) <- c(
#'   "Term", "B", "SE", "t", "p",
#'   "CI_lower", "CI_upper"
#' )
#' nice_table(stats.table, highlight = TRUE)
#'
#' # Test different column names
#' test <- head(mtcars)
#' names(test) <- c(
#'   "dR", "N", "M", "SD", "b", "np2",
#'   "ges", "p", "r", "R2", "sr2"
#' )
#' test[, 10:11] <- test[, 10:11] / 10
#' nice_table(test)
#'
#' # Custom cell formatting (such as p or r)
#' nice_table(test[8:11], col.format.p = 2:4, highlight = .001)
#'
#' nice_table(test[8:11], col.format.r = 1:4)
#'
#' # Apply custom functions to cells
#' fun <- function(x) {
#'   x + 11.1
#' }
#' nice_table(test[8:11], col.format.custom = 2:4, format.custom = "fun")
#'
#' fun <- function(x) {
#'   paste("x", x)
#' }
#' nice_table(test[8:11], col.format.custom = 2:4, format.custom = "fun")
#'
#' # Separate headers based on periods
#' header.data <- structure(
#'   list(
#'     Variable = c(
#'       "Sepal.Length",
#'       "Sepal.Width", "Petal.Length"
#'     ), setosa.M = c(
#'       5.01, 3.43,
#'       1.46
#'     ), setosa.SD = c(0.35, 0.38, 0.17), versicolor.M =
#'       c(5.94, 2.77, 4.26), versicolor.SD = c(0.52, 0.31, 0.47)
#'   ),
#'   row.names = c(NA, -3L), class = "data.frame"
#' )
#' nice_table(header.data,
#'   separate.header = TRUE,
#'   italics = 2:4
#' )
#'
#' @importFrom dplyr mutate %>% select matches
#' case_when relocate across contains select_if any_of
#' last_col
#' @importFrom rlang :=
#'
#' @seealso
#' Tutorial: \url{https://rempsyc.remi-theriault.com/articles/table}
#'

#' @export
nice_table <- function(
  data,
  highlight = FALSE,
  stars = TRUE,
  italics,
  col.format.p,
  col.format.r,
  col.format.ci,
  format.custom,
  col.format.custom,
  width = NULL,
  spacing = 2,
  broom = NULL,
  report = NULL,
  short = FALSE,
  title,
  note,
  separate.header
) {
  rlang::check_installed(
    c("flextable", "methods"),
    version = c(get_dep_version("flextable"), NA),
    reason = "for this function."
  )

  if (!inherits(data, "data.frame")) {
    message("Non-dataframe detected. Attempting to coerce to dataframe")
  }

  dataframe <- as.data.frame(data)

  format_p_internal <- ifelse(isTRUE(stars), "format_p_stars", "format_p")

  #   _______________________________________
  #   Replace possible alternative names ####
  dataframe <- dataframe %>%
    rename_with(
      ~ case_when(
        . == "p.value" ~ "p",
        . == "pvalue" ~ "p",
        . == "chisq" ~ "chi2",
        . == "conf.high" ~ "CI_upper",
        . == "df_error" ~ "df",
        . == "Cohens_d" ~ "d",
        . == "Coefficient" ~ "b",
        . == "t.ratio" ~ "t",
        . == "Std_Coefficient" ~ "B",
        . == "r_rank_biserial" ~ "rrb",
        . == "Eta2" ~ "n2",
        . == "Eta2_partial" ~ "np2",
        . == "mu" ~ "Mu",
        . == "term" ~ "Term",
        . == "method" ~ "Method",
        . == "alternative" ~ "Alternative",

        # confidence intervals
        # classic broom style
        . == "conf.low" ~ "CI_lower",
        . == "conf.high" ~ "CI_upper",

        # effectsize/parameters style
        . == "CI_low" ~ "CI_lower",
        . == "CI_high" ~ "CI_upper",

        # lowercase ci style
        . == "ci_low" ~ "CI_lower",
        . == "ci_high" ~ "CI_upper",

        # broom::confint_tidy style
        . == "lwr.ci" ~ "CI_lower",
        . == "upr.ci" ~ "CI_upper",

        # emmeans style
        . == "lower.CL" ~ "CI_lower",
        . == "upper.CL" ~ "CI_upper",

        # asymptotic CI style
        . == "asymp.LCL" ~ "CI_lower",
        . == "asymp.UCL" ~ "CI_upper",

        # Bayesian HDI style
        . == "hdi_low" ~ "CI_lower",
        . == "hdi_high" ~ "CI_upper",

        # quantile style
        . == "Q2.5" ~ "CI_lower",
        . == "Q97.5" ~ "CI_upper",
        . == "2.5 %" ~ "CI_lower",
        . == "97.5 %" ~ "CI_upper",

        # misc
        . == "lo95" ~ "CI_lower",
        . == "hi95" ~ "CI_upper",
        . == "LB" ~ "CI_lower",
        . == "UB" ~ "CI_upper",

        # everything else
        TRUE ~ .
      )
    )

  #   __________________________________
  #   Broom integration            ####

  dataframe <- prepare_broom(dataframe, broom)

  #   __________________________________
  #   Report integration           ####

  dataframe <- prepare_report(dataframe, report, short)

  #   _________________________________
  #   Formatting                   ####

  if (!missing(separate.header)) {
    filtered.names <- grep("[.]", names(dataframe), value = TRUE)
    sh.pattern <- lapply(filtered.names, function(x) {
      gsub("[^\\.]*$", "", x)
    }) %>%
      unlist() %>%
      unique()
    unique.pattern <- length(sh.pattern)
  }

  dataframe <- prepare_flextable(
    dataframe,
    separate.header,
    col.format.ci,
    highlight,
    sh.pattern,
    unique.pattern
  )

  #   __________________________________
  #   Flextable                     ####

  nice.borders <- list("width" = 0.5, color = "black", style = "solid")

  table <- create_flextable(
    dataframe,
    highlight,
    width,
    spacing,
    note,
    separate.header,
    nice.borders
  )

  #   ___________________________________
  #   Column formatting              ####

  table <- format_columns(
    dataframe,
    table,
    italics,
    separate.header,
    highlight,
    sh.pattern,
    unique.pattern,
    format_p_internal
  )

  #   _____________________________________________
  #   Extra features                           ####

  table <- beautify_flextable(
    dataframe,
    table,
    separate.header,
    col.format.p,
    col.format.r,
    format.custom,
    col.format.custom,
    sh.pattern,
    unique.pattern,
    format_p_internal
  )

  #   ___________________________
  #   Final touch up (title) ####

  table <- finalize_table(table, title, nice.borders)

  # class(table) <- c("nice_table", class(table))
  # remove `nice_table` class because of name collision with printing method
  # of the `afex` package for `nice_table` objects

  table
}

#   ____________________________________________________________________________
#   Other functions                                                         ####

# format_CI
format_CI <- function(
  dataframe,
  CI_low_high = c("CI_lower", "CI_upper"),
  col.name = "95% CI"
) {
  dataframe %>%
    mutate(across(all_of(CI_low_high), function(x) {
      x %>%
        as.numeric() %>%
        round(2) %>%
        formatC(2, format = "f")
    })) %>%
    mutate(
      !!col.name := paste0(
        "[",
        .[[CI_low_high[1]]],
        ", ",
        .[[CI_low_high[2]]],
        "]"
      )
    ) %>%
    select(-all_of(CI_low_high))
}

# format_flex
format_flex <- function(table, j, digits = 2, value, fun) {
  if (missing(value)) {
    table <- table %>%
      flextable::italic(j = j, part = "header") %>%
      flextable::colformat_double(j = j, big.mark = ",", digits = digits)
  } else {
    rExpression <- paste0("flextable::as_paragraph(", value, ")")
    table <- table %>%
      flextable::compose(
        i = NULL,
        j = j,
        part = "header",
        value = eval(parse(text = rExpression))
      )
  }
  if (!missing(fun)) {
    table <- table %>%
      parse_formatter(column = j, fun = fun)
  }
  table
}

# parse_formatter
parse_formatter <- function(
  table,
  call = "table <- table %>% flextable::set_formatter",
  column,
  fun
) {
  rExpression <- paste0(call, "(`", column, "` = ", fun, ")")
  eval(parse(text = rExpression))
}

# prepare_broom
prepare_broom <- function(dataframe, broom) {
  if (!is.null(broom)) {
    dataframe <- dataframe %>%
      rename_with(
        ~ case_when(
          . == "statistic" ~ "t",
          . == "std.error" ~ "SE",
          . == "parameter" ~ "df",
          TRUE ~ .
        )
      )
    if (broom == "t.test") {
      dataframe <- dataframe %>%
        relocate(estimate, .after = estimate2) %>%
        rename_with(
          ~ case_when(
            . == "estimate" ~ "M1 - M2",
            . == "estimate1" ~ "Mean 1",
            . == "estimate2" ~ "Mean 2",
            TRUE ~ .
          )
        ) %>%
        relocate(df, .before = p) %>%
        relocate(Method:Alternative)
    } else if (broom == "lm") {
      dataframe <- dataframe %>%
        rename_with(
          ~ case_when(
            . == "estimate" ~ "b",
            TRUE ~ .
          )
        )
    } else if (broom == "cor.test") {
      dataframe <- dataframe %>%
        rename_with(
          ~ case_when(
            . == "estimate" ~ "r",
            TRUE ~ .
          )
        ) %>%
        relocate(df, .before = p) %>%
        relocate(Method:Alternative, .before = r)
    } else if (broom == "wilcox.test") {
      dataframe <- dataframe %>%
        rename_with(
          ~ case_when(
            . == "t" ~ "W",
            TRUE ~ .
          )
        ) %>%
        relocate(Method:Alternative, .before = W)
    }
  }
  dataframe
}

# prepare_report
prepare_report <- function(dataframe, report, short) {
  if (!is.null(report) || any(class(dataframe) == "report_table")) {
    # t.test, aov, and wilcox need to be done separately
    # because they have no model_class attribute
    if ("Method" %in% names(dataframe)) {
      if (all(grepl("t-test", dataframe$Method))) {
        report <- "t.test"
      } else if (all(grepl("Wilcox", dataframe$Method))) {
        report <- "wilcox"
      }
    } else if ("Sum_Squares" %in% names(dataframe)) {
      report <- "aov"
    } else if (length(attr(dataframe, "model_class")) > 0) {
      if ("lm" %in% attr(dataframe, "model_class")) {
        report <- "lm"
      } else if (grepl("correlation", attr(dataframe, "title"))) {
        report <- "cor.test"
      }
    }
    dataframe <- dataframe %>%
      relocate(any_of(c("method", "alternative"))) %>%
      select(-any_of("CI"))
    if (report == "t.test") {
      dataframe <- dataframe %>%
        format_CI(col.name = "95% CI (t)") %>%
        relocate(`95% CI (t)`, .after = t)

      dataframe <- dataframe %>%
        format_CI(c("d_CI_low", "d_CI_high"), col.name = "95% CI (d)")
      if (short == TRUE) {
        dataframe <- dataframe %>%
          select_if(
            !names(.) %in%
              c(
                "method",
                "alternative",
                "Mean_Group1",
                "Mean_Group2",
                "Difference",
                "95% CI (t)"
              )
          )
      }
    } else if (report == "lm") {
      dataframe <- dataframe %>%
        relocate(Fit, .after = Parameter)
      dataframe <- dataframe %>%
        format_CI(col.name = "95% CI (b)") %>%
        relocate(`95% CI (b)`, .after = b)

      dataframe <- dataframe %>%
        format_CI(
          c("Std_Coefficient_CI_low", "Std_Coefficient_CI_high"),
          col.name = "95% CI (B)"
        )
      if (short == TRUE) {
        dataframe <- select(dataframe, -c("Fit", "95% CI (b)"))
        dataframe <- dataframe[
          -(which(is.na(dataframe$Parameter))[1]:nrow(dataframe)),
        ]
      }
    } else if (report == "aov") {
      if ("Eta2_CI_low" %in% names(dataframe)) {
        dataframe <- dataframe %>%
          format_CI(c("Eta2_CI_low", "Eta2_CI_high"), col.name = "95% CI (n2)")
      }
      if ("Eta2_partial_CI_low" %in% names(dataframe)) {
        dataframe <- dataframe %>%
          format_CI(
            c("Eta2_partial_CI_low", "Eta2_partial_CI_high"),
            col.name = "95% CI (np2)"
          )
      }
    } else if (report == "wilcox") {
      dataframe <- dataframe %>%
        format_CI(
          c("rank_biserial_CI_low", "rank_biserial_CI_high"),
          col.name = "95% CI (rrb)"
        )
    }
  }
  if (all(c("Cohens_d_CI_low", "Cohens_d_CI_high") %in% names(dataframe))) {
    dataframe <- dataframe %>%
      format_CI(
        c("Cohens_d_CI_low", "Cohens_d_CI_high"),
        col.name = "95% CI (d)"
      )
  }
  if (
    all(
      c("Std_Coefficient_CI_low", "Std_Coefficient_CI_high") %in%
        names(dataframe)
    )
  ) {
    dataframe <- dataframe %>%
      format_CI(
        c("Std_Coefficient_CI_low", "Std_Coefficient_CI_high"),
        col.name = "95% CI (B)"
      )
  }
  dataframe
}

# prepare_flextable
prepare_flextable <- function(
  dataframe,
  separate.header,
  col.format.ci,
  highlight,
  sh.pattern,
  unique.pattern
) {
  if (!missing(col.format.ci)) {
    if (!methods::is(col.format.ci, "list")) {
      col.format.ci <- list(col.format.ci)
    }
    for (i in col.format.ci) {
      ci.name <- paste0(sh.pattern[i], "95% CI")
      dataframe <- format_CI(
        dataframe,
        i,
        col.name = ci.name
      ) %>%
        relocate(
          all_of(ci.name),
          .after = select(
            .,
            contains(sh.pattern),
            -last_col()
          ) %>%
            select(last_col()) %>%
            names()
        )
    }
  }
  if ("CI_lower" %in% names(dataframe) && "CI_upper" %in% names(dataframe)) {
    dataframe <- format_CI(dataframe)
  }
  if (
    "CI_lower_B" %in% names(dataframe) && "CI_upper_B" %in% names(dataframe)
  ) {
    dataframe <- dataframe %>%
      rename("95% CI (b)" = "95% CI") %>%
      relocate("B", .after = last_col()) %>%
      format_CI(c("CI_lower_B", "CI_upper_B"), col.name = "95% CI (B)")
  }
  if (
    "CI_lower_r" %in% names(dataframe) && "CI_upper_r" %in% names(dataframe)
  ) {
    dataframe <- dataframe %>%
      rename("95% CI (sigma)" = "95% CI") %>%
      relocate("r", .after = last_col()) %>%
      format_CI(c("CI_lower_r", "CI_upper_r"), col.name = "95% CI (r)")
  }
  if (
    "rmsea.ci.lower" %in%
      names(dataframe) &&
      "rmsea.ci.upper" %in% names(dataframe)
  ) {
    dataframe <- format_CI(
      dataframe,
      c(
        "rmsea.ci.lower",
        "rmsea.ci.upper"
      ),
      "90% CI (RMSEA)"
    )
    if ("rmsea" %in% names(dataframe)) {
      relocate(dataframe, "90% CI (RMSEA)", .after = "rmsea")
    }
  }
  if (!missing(separate.header)) {
    CI_lower.sh <- paste0(
      sh.pattern,
      rep(
        "CI_lower",
        each = unique.pattern
      )
    )
    CI_upper.sh <- paste0(
      sh.pattern,
      rep(
        "CI_upper",
        each = unique.pattern
      )
    )
    CI.df <- data.frame(CI_lower.sh, CI_upper.sh)
    names(CI.df) <- NULL
    if (any(unlist(CI.df) %in% names(dataframe))) {
      for (i in seq(nrow(CI.df))) {
        ci.name <- paste0(sh.pattern[i], "95% CI")
        dataframe <- format_CI(
          dataframe,
          CI_low_high = unlist(CI.df[i, ]),
          col.name = ci.name
        ) %>%
          relocate(
            all_of(ci.name),
            .after = select(
              .,
              contains(sh.pattern[i]),
              -last_col()
            ) %>%
              select(last_col()) %>%
              names()
          )
      }
    }
  }
  dataframe <- dataframe %>%
    mutate(across(
      contains("95% CI"),
      ~ ifelse(
        .x == "[ NA,  NA]",
        "",
        .x
      )
    ))

  if ("p" %in% names(dataframe) && isTRUE(highlight) || is.numeric(highlight)) {
    highlight <- ifelse(isTRUE(highlight), .05, highlight)
    dataframe <- dataframe %>%
      mutate(signif = ifelse(p < highlight, TRUE, FALSE))
  }
  if ("Predictor" %in% names(dataframe)) {
    dataframe$Predictor <- gsub(
      ":",
      " \u00D7 ",
      dataframe$Predictor
    )
  }
  if ("Term" %in% names(dataframe)) {
    dataframe$Term <- gsub(
      ":",
      " \u00D7 ",
      dataframe$Term
    )
  }
  if ("Model Number" %in% names(dataframe)) {
    dataframe <- dataframe %>%
      select(-all_of("Model Number"))
  }
  # Capitals
  cols.capitals <- c(
    "cfi",
    "tli",
    "nnfi",
    "rfi",
    "nfi",
    "pnfi",
    "ifi",
    "rni",
    "logl",
    "aic",
    "bic",
    "bic2",
    "rmsea",
    "rmr",
    "srmr",
    "crmr",
    "gfi",
    "agfi",
    "pgfi",
    "mfi",
    "ecvi"
  )
  dataframe <- dataframe %>%
    rename_with(.fn = toupper, .cols = any_of(cols.capitals))
  dataframe
}

# create_flextable
create_flextable <- function(
  dataframe,
  highlight,
  width,
  spacing,
  note,
  separate.header,
  nice.borders
) {
  table <- dataframe %>%
    {
      if (
        "p" %in% names(dataframe) && highlight == TRUE || is.numeric(highlight)
      ) {
        flextable::flextable(., col_keys = names(dataframe)[-length(dataframe)])
      } else {
        flextable::flextable(.)
      }
    }

  # Merge cells for repeated dependent variables...
  if (
    "Dependent Variable" %in%
      names(dataframe) &&
      any(duplicated(dataframe$`Dependent Variable`))
  ) {
    model.row <- which(
      !duplicated(dataframe$`Dependent Variable`, fromLast = TRUE)
    )
    table <- table %>%
      flextable::merge_v(j = "Dependent Variable") %>%
      flextable::hline(i = model.row, border = nice.borders)
  }

  table <- table %>%
    flextable::hline_top(part = "head", border = nice.borders) %>%
    flextable::hline_bottom(part = "head", border = nice.borders) %>%
    flextable::hline_top(part = "body", border = nice.borders) %>%
    flextable::hline_bottom(part = "body", border = nice.borders) %>%
    flextable::align(align = "center", part = "all") %>%
    flextable::align(j = 1, align = "left", part = "body") %>%
    flextable::valign(valign = "center", part = "all") %>%
    flextable::line_spacing(space = spacing, part = "all") %>%
    flextable::fix_border_issues()

  if (!is.null(width)) {
    table <- table %>%
      flextable::set_table_properties(layout = "autofit", width = width)
  } else {
    table <- table %>%
      flextable::set_table_properties(layout = "autofit")
  }

  if (!missing(note)) {
    note.list <- as.list(note)
    table <- table %>%
      flextable::add_footer_lines("") %>%
      flextable::compose(
        i = 1,
        j = 1,
        value = flextable::as_paragraph(
          flextable::as_i("Note. "),
          note[[1]]
        ),
        part = "footer"
      ) %>%
      flextable::align(part = "footer", align = "left")
    if (length(note.list) > 1) {
      table <- table %>%
        flextable::add_footer_lines(note.list[-1])
    }
  }

  # Separate headers
  if (!missing(separate.header)) {
    table <- table %>%
      flextable::separate_header("span-top", split = "[.]")
  }

  table <- table %>%
    flextable::fontsize(part = "all", size = 12) %>%
    flextable::font(part = "all", fontname = "Times New Roman")
  table
}

format_columns <- function(
  dataframe,
  table,
  italics,
  separate.header,
  highlight,
  sh.pattern,
  unique.pattern,
  format_p_internal
) {
  ##  ....................................
  ##  Special cases                  ####
  # Fix header with italics
  if (!missing(italics) & missing(separate.header)) {
    table <- table %>%
      flextable::italic(j = italics, part = "header")
  } else if (!missing(italics) & !missing(separate.header)) {
    level.number <- sum(
      charToRaw(names(
        dataframe[2]
      )) ==
        charToRaw(".")
    ) +
      1
    table <- table %>%
      flextable::italic(j = italics, i = level.number, part = "header")
  }

  # Degrees of freedom
  cols.df <- "df"
  if (!missing(separate.header)) {
    cols.df.sh <- paste0(
      sh.pattern,
      rep(
        cols.df,
        each = unique.pattern
      )
    )
    cols.df <- c(cols.df, cols.df.sh)
  }

  for (i in cols.df) {
    if (i %in% names(dataframe)) {
      df.digits <- ifelse(any(dataframe[i] %% 1 == 0), 0, 2)
      table <- table %>%
        format_flex(j = i, digits = df.digits)
    }
  }

  ##  .....................................
  ##  2-digit columns                 ####

  # Italicize all these column names
  cols.2digits <- c(
    "t",
    "SE",
    "SD",
    "F",
    "b",
    "M",
    "W",
    "d",
    "g",
    "Mu",
    "S",
    "z",
    "Z"
  )
  if (!missing(separate.header)) {
    cols.2digits.sh <- paste0(
      sh.pattern,
      rep(
        cols.2digits,
        each = unique.pattern
      )
    )
    cols.2digits <- c(cols.2digits, cols.2digits.sh)
  }

  for (i in cols.2digits) {
    if (i %in% names(dataframe)) {
      table <- table %>%
        format_flex(j = i)
    }
  }

  ##  .....................................
  ##  0-digit columns                 ####
  cols.0digits <- c("N", "n")
  if (!missing(separate.header)) {
    cols.0digits.sh <- paste0(
      sh.pattern,
      rep(
        cols.0digits,
        each = unique.pattern
      )
    )
    cols.0digits <- c(cols.0digits, cols.0digits.sh)
  }

  for (i in cols.0digits) {
    if (i %in% names(dataframe)) {
      table <- table %>%
        format_flex(j = i, digits = 0)
    }
  }

  ##  .....................................
  ##  Formatting functions            ####
  compose.table0 <- data.frame(
    col = c("p", "r"),
    fun = c(format_p_internal, "format_r")
  )

  if (!missing(separate.header)) {
    cols.sh <- paste0(
      sh.pattern,
      rep(
        compose.table0$col,
        each = unique.pattern
      )
    )
    table0.sh <- data.frame(
      col = cols.sh,
      fun = rep(
        compose.table0$fun,
        each = length(cols.sh) / length(compose.table0$fun)
      )
    )
    compose.table0 <- rbind(compose.table0, table0.sh)
  }

  for (i in seq(nrow(compose.table0))) {
    if (compose.table0[i, "col"] %in% names(dataframe)) {
      table <- table %>%
        format_flex(
          j = compose.table0[i, "col"],
          fun = compose.table0[i, "fun"]
        )
    }
  }

  ##  .....................................
  ##  Special symbols                 ####
  compose.table1 <- data.frame(
    col = c(
      "95% CI (b)",
      "95% CI (B)",
      "95% CI (t)",
      "95% CI (d)",
      "95% CI (np2)",
      "95% CI (n2)",
      "95% CI (rrb)",
      "95% CI (sigma)",
      "95% CI (sigma2)",
      "95% CI (r)",
      "np2",
      "n2",
      "ges",
      "dR",
      "Predictor (+/-1 SD)",
      "M1 - M2",
      "tau",
      "rho",
      "rrb",
      "chi2",
      "chi2.df",
      "B",
      "sigma",
      "sigma2"
    ),
    value = c(
      '"95% CI (", flextable::as_i("b"), ")"', # small b
      '"95% CI (", flextable::as_i("b"), "*", ")"', # beta
      '"95% CI (", flextable::as_i("t"), ")"', # t
      '"95% CI (", flextable::as_i("d"), ")"', # d
      '"95% CI (", "\u03b7", flextable::as_sub("p"), flextable::as_sup("2"), ")"', # peta
      '"95% CI (", "\u03b7", flextable::as_sup("2"), ")"', # eta
      '"95% CI (", flextable::as_i("r"), flextable::as_i(flextable::as_sub("rb")), ")"', # rrb
      '"95% CI (", "\u03C3", ")"', # sigma
      '"95% CI (", "\u03C3", flextable::as_sup("2"), ")"', # sigma2
      '"95% CI (", flextable::as_i("r"), ")"', # r
      '"\u03b7", flextable::as_sub("p"), flextable::as_sup("2")', # eta
      '"\u03b7", flextable::as_sup("2")', # eta
      '"\u03b7", flextable::as_sub("G"), flextable::as_sup("2")', # eta
      'flextable::as_i("d"), flextable::as_sub("R")',
      '"Predictor (+/-1 ", flextable::as_i("SD"), ")"',
      'flextable::as_i("M"), flextable::as_sub("1"), " - ", flextable::as_i("M"), flextable::as_sub("2")',
      '"\u03C4"', # tau
      '"\u03C1"', # rho
      'flextable::as_i("r"), flextable::as_i(flextable::as_sub("rb"))',
      '"\u03C7", flextable::as_sup("2")', # Chi square
      '"\u03C7", flextable::as_sup("2"), "\u2215", flextable::as_i("df")', # Chi square
      'flextable::as_i("b"), "*"', # beta
      '"\u03C3"', # sigma
      '"\u03C3", flextable::as_sup("2")' # sigma
    )
  )
  for (i in seq(nrow(compose.table1))) {
    if (compose.table1[i, "col"] %in% names(dataframe)) {
      table <- table %>%
        format_flex(
          j = compose.table1[i, "col"],
          value = compose.table1[i, "value"]
        )
    }
  }
  # Values that can't be greater than 1, but not just italic formatting like r
  compose.table2 <- data.frame(
    col = c("R2", "sr2", "CFI", "TLI", "RMSEA", "SRMR"),
    value = c(
      'flextable::as_i("R"), flextable::as_sup("2")',
      'flextable::as_i("sr"), flextable::as_sup("2")',
      "CFI",
      "TLI",
      "RMSEA",
      "SRMR"
    )
  )

  if (!missing(separate.header)) {
    cols.sh <- paste0(
      sh.pattern,
      rep(
        compose.table2$col,
        each = unique.pattern
      )
    )
    table2.sh <- data.frame(
      col = cols.sh,
      value = rep(
        compose.table2$value,
        each = length(cols.sh) / length(compose.table2$value)
      )
    )
    compose.table2 <- rbind(compose.table2, table2.sh)
  }

  for (i in seq(nrow(compose.table2))) {
    if (compose.table2[i, "col"] %in% names(dataframe)) {
      table <- table %>%
        format_flex(
          j = compose.table2[i, "col"],
          value = compose.table2[i, "value"],
          fun = "format_r"
        )
    }
  }

  if ("p" %in% names(dataframe) && isTRUE(highlight) || is.numeric(highlight)) {
    table <- table %>%
      flextable::bold(
        i = ~ signif == TRUE,
        j = table$col_keys
      ) %>%
      flextable::bg(
        i = ~ signif == TRUE,
        j = table$col_keys,
        bg = "#D9D9D9"
      )
  }

  # Set attributes for variables which digits should not be changed for later step
  attr(table, "dont_change") <- c(
    compose.table0$col,
    cols.df,
    cols.2digits,
    cols.0digits,
    compose.table2$col
  )
  table
}

beautify_flextable <- function(
  dataframe,
  table,
  separate.header,
  col.format.p,
  col.format.r,
  format.custom,
  col.format.custom,
  sh.pattern,
  unique.pattern,
  format_p_internal
) {
  dont.change0 <- attr(table, "dont_change")
  dont.change <- paste0("^", dont.change0, "$", collapse = "|")

  if (!missing(separate.header)) {
    dont.change.sh <- paste0(
      sh.pattern,
      rep(
        dont.change0,
        each = unique.pattern
      )
    )
    dont.change.sh <- paste0("^", dont.change.sh, "$", collapse = "|")
    dont.change <- paste0(dont.change, "|", dont.change.sh)
  }

  table <- table %>%
    flextable::colformat_double(
      j = (select(dataframe, where(is.numeric)) %>%
        select(-matches(dont.change, ignore.case = FALSE)) %>%
        names()),
      big.mark = ",",
      digits = 2
    )
  if (!missing(col.format.p)) {
    table <- table %>%
      parse_formatter(
        column = table$col_keys[col.format.p],
        fun = format_p_internal
      )
  }
  if (!missing(col.format.r)) {
    table <- table %>%
      parse_formatter(
        column = table$col_keys[col.format.r],
        fun = "format_r"
      )
  }
  if (!missing(format.custom) & !missing(col.format.custom)) {
    # table %>%
    #   parse_formatter(column = table$col_keys[col.format.custom],
    #                   fun = "format.custom") -> table
    # Error in set_formatter: object 'format.custom' not found
    rExpression <- paste0(
      "table <- table %>% flextable::set_formatter(`",
      table$col_keys[col.format.custom],
      "` = ",
      format.custom,
      ")"
    )
    eval(parse(text = rExpression))
  }
  table
}

finalize_table <- function(table, title, nice.borders) {
  if (!missing(title)) {
    invisible.borders <- flextable::fp_border_default("width" = 0)
    italic.lvl <- ifelse(length(title) == 1, 1, 2)
    bold.decision <- ifelse(length(title) == 1, FALSE, TRUE)

    table <- table %>%
      flextable::add_header_lines(values = title) %>%
      flextable::align(
        part = "header",
        i = seq(length(title)),
        align = "left"
      ) %>%
      flextable::hline(
        part = "header",
        i = seq_len(length(title) - 1),
        border = invisible.borders
      ) %>%
      flextable::hline(
        part = "header",
        i = length(title),
        border = nice.borders
      ) %>%
      flextable::hline_top(border = invisible.borders, part = "header") %>%
      # flextable::border(part = "header", i = 1:length(title),
      #        border = invisible.borders) %>%
      flextable::italic(part = "header", i = italic.lvl) %>%
      flextable::bold(., part = "header", i = 1, bold = bold.decision)
  }
  table
}

Try the rempsyc package in your browser

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

rempsyc documentation built on Sept. 15, 2025, 1:07 a.m.