diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1ccdb03c..f519e60e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,7 +13,7 @@ "packages": "qpdf" }, "ghcr.io/rocker-org/devcontainer-features/r-packages:1": { - "packages": "usethis,pak,qpdf,cli,flextable,forstringr,fs,naniar,officer,pdftools,prodlim,svDialogs,tidyselect,tinytex", + "packages": "usethis,pak,qpdf,cli,gt,forstringr,fs,naniar,officer,pdftools,prodlim,svDialogs,tidyselect,tinytex", "installSystemRequirements": true }, // option to run rstudio. you can type rserver into the command line to diff --git a/DESCRIPTION b/DESCRIPTION index c4f4f8d5..1e3eaab9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -25,12 +25,11 @@ Imports: cli, data.table, dplyr, - flextable, forstringr, fs, glue, + gt, naniar, - officer, purrr, stats, stringi, @@ -42,7 +41,6 @@ Imports: utils Suggests: ggplot2, - gt, kableExtra, knitr, parallel, diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8c0df26d..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Schiano-NOAA - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/NAMESPACE b/NAMESPACE index 6f069d1d..6315cdcf 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(ID_tbl_length_class) export(ID_tbl_width_class) export(add_accessibility) export(add_alttext) diff --git a/R/ID_tbl_length_class.R b/R/ID_tbl_length_class.R new file mode 100644 index 00000000..303cb08d --- /dev/null +++ b/R/ID_tbl_length_class.R @@ -0,0 +1,47 @@ +#' Identify table length class +#' +#' @inheritParams render_lg_table +#' +#' @return The length class of a table: regular or long. The result +#' will determine whether the table can be rendered on a page +#' as-is, or if it needs to be split across multiple pages. +#' @export +#' +#' @examples +#' \dontrun{ +#' ID_tbl_length_class( +#' plot_name = "indices.abundance_table.rda", +#' tables_dir = here::here() +#' ) +#' } +ID_tbl_length_class <- function( + tables_dir, + plot_name +) { + tables_path <- fs::path( + tables_dir, + "tables", + paste0(plot_name, "_table.rda") + ) + + if (file.exists(tables_path)) { + load(tables_path) + table_rda <- rda + rm(rda) + gt_table <- table_rda$table + + # Get table length in rows + table_length <- nrow(gt_table[["_data"]]) + + # determine table length class + if (table_length <= 38) { + length_class <- "regular" # fit on one landscape page (and, by default, portrait) + } else { + length_class <- "long" # divided across >1 landscape page + } + } else { + cli::cli_abort(message = "Table not found at {tables_path}") + } + + length_class +} diff --git a/R/ID_tbl_width_class.R b/R/ID_tbl_width_class.R index 07b6238d..9e83d3c0 100644 --- a/R/ID_tbl_width_class.R +++ b/R/ID_tbl_width_class.R @@ -7,7 +7,7 @@ #' be resized, rotated, and/or split across multiple pages. #' #' @return The width class of a table: regular, wide, or extra-wide. The result -#' will determine whether the table that can be rendered on a portrait page +#' will determine whether the table can be rendered on a portrait page #' as-is, or if it needs to be resized, rotated, and/or split across multiple pages. #' @export #' @@ -34,9 +34,14 @@ ID_tbl_width_class <- function( load(tables_path) table_rda <- rda rm(rda) - table_width <- flextable::flextable_dim(table_rda$table)[["widths"]] |> - as.numeric() + gt_table <- table_rda$table + # Set each col to 1.5" + col_inches <- 1.5 + + # Calculate total table width in inches + table_width <- ncol(gt_table[["_data"]]) * col_inches + # determine table width class if (table_width <= portrait_pg_width) { width_class <- "regular" diff --git a/R/add_child.R b/R/add_child.R index 31ba9fa5..7e03bcce 100644 --- a/R/add_child.R +++ b/R/add_child.R @@ -28,6 +28,10 @@ add_child <- function(x, "#| warning: false", "\n", "a <- knitr::knit_child(", "'", sec_num, "'", ", quiet = TRUE)", "\n", "cat(a, sep = '\\n')", "\n", + # ifelse checks for the 'tables' label and injects the extra code/comment + ifelse(label[i] == "tables", + "# Force LaTeX to render all floating tables before moving to the next section\ncat('\\n\\\\clearpage\\n')\n", + ""), "```", "\n" ) child <- paste(child, child_loop, sep = "\n {{< pagebreak >}} \n") diff --git a/R/asar-package.R b/R/asar-package.R index ee23b68a..16039ed9 100644 --- a/R/asar-package.R +++ b/R/asar-package.R @@ -17,6 +17,6 @@ globvar <- c( "row_rescale", "table_list", "read.csv", "All", "x", "Acronym", "Meaning", "meaning_lower", "Definition", "n", "Label", "figures_doc_header", "X", "len_bins", "area", "match_key", "uncertainty_label", - "word_count", "ac_short" + "word_count", "ac_short", "column_label" ) if (getRversion() >= "2.15.1") utils::globalVariables(globvar) diff --git a/R/create_tables_doc.R b/R/create_tables_doc.R index 9e78926d..6167fdce 100644 --- a/R/create_tables_doc.R +++ b/R/create_tables_doc.R @@ -77,7 +77,7 @@ create_tables_doc <- function(subdir = getwd(), tables_doc_setup <- paste0( add_chunk( glue::glue( - "library(flextable) + "library(gt) tables_dir <- fs::path('{tables_dir}', 'tables')" ), label = "set-rda-dir-tbls", @@ -122,15 +122,42 @@ create_tables_doc <- function(subdir = getwd(), ) # identify table orientation - # split tables will always be extra_wide + # split tables will always be extra-wide tbl_orient <- ifelse(split, - "extra_wide", + "extra-wide", ID_tbl_width_class( plot_name = tab_shortname, tables_dir = tables_dir, portrait_pg_width = portrait_pg_width ) ) + + # identify table length: regular (1 landscape page) or long (>1 landscape page) + tbl_length <- ID_tbl_length_class( + plot_name = tab_shortname, + tables_dir = tables_dir + ) + + table_specs <- list(tbl_orient, tbl_length) + + tbl_class <- dplyr::case_when( + table_specs[[1]] == "regular" & table_specs[[2]] == "regular" ~ "reg_reg", # 38 rows / portrait + table_specs[[1]] == "regular" & table_specs[[2]] == "long" ~ "reg_long", # 38 rows, split / portrait + table_specs[[1]] == "wide" & table_specs[[2]] == "regular" ~ "wide_reg", # 28 rows / landscape + table_specs[[1]] == "wide" & table_specs[[2]] == "long" ~ "wide_long", # 28 rows, split / landscape + table_specs[[1]] == "extra-wide" & table_specs[[2]] == "regular" ~ "ewide_reg", # 28 rows, split / landscape + table_specs[[1]] == "extra-wide" & table_specs[[2]] == "long" ~ "ewide_long", # 28 rows, split / landscape + TRUE ~ "unknown" + ) + + if (tbl_class == "unknown"){ + cli::cli_abort("Unknown table class. Check table is an acceptable `gt` table.") + } + + # set max number of rows per table based on orientation + max_rows <- ifelse(tbl_orient == "regular", + 38, # max rows for portrait + 28) # max rows for landscape ## import table, caption ## do this for all tables @@ -150,11 +177,15 @@ load(file.path(tables_dir, '", stringr::str_remove(tab, "_split"), "'))\n "\n" ) - ## add table if it only requires one chunk - if (tbl_orient == "regular") { + ## add table if it is intact on a portrait page + if (tbl_class == "reg_reg") { tables_doc_plot_setup2 <- paste0( add_chunk( - glue::glue("{tab_shortname}_table"), + glue::glue("{tab_shortname}_table |>\n", + " gt::cols_width(\n", + " everything() ~ pct(20)\n", + " ) \n" + ), label = glue::glue("tbl-{tab_shortname}"), # add_option = TRUE, chunk_option = c( @@ -162,21 +193,29 @@ load(file.path(tables_dir, '", stringr::str_remove(tab, "_split"), "'))\n "warnings: false", glue::glue( "tbl-cap: !expr {tab_shortname}_cap" - ) + ), + "tbl-pos: 't'" ) ), "\n" ) } - - if (tbl_orient == "wide") { + + ## add table if it is intact, rotated on a landscape page + if (tbl_class == "wide_reg") { tables_doc_plot_setup2 <- paste0( # add landscape braces before R chunk "::: {.landscape}\n\n", add_chunk( glue::glue( - "{tab_shortname}_table |> - flextable::fit_to_width(max_width = 8)" + "{tab_shortname}_table |>\n", + " gt::tab_options(\n", + " table.width = pct(100),\n", + " table.layout = 'auto'\n", + " ) |>\n", + " gt::cols_width(\n", + " everything() ~ pct(20)\n", + " ) \n" ), label = glue::glue("tbl-{tab_shortname}"), # add_option = TRUE, @@ -185,7 +224,8 @@ load(file.path(tables_dir, '", stringr::str_remove(tab, "_split"), "'))\n "warnings: false", glue::glue( "tbl-cap: !expr {tab_shortname}_cap" - ) + ), + "tbl-pos: 't'" ) ), "\n", @@ -194,21 +234,80 @@ load(file.path(tables_dir, '", stringr::str_remove(tab, "_split"), "'))\n ) } - if (tbl_orient == "extra_wide") { + ## add table if it is long enough to be shown on >1 portrait ("reg_long") OR landscape ("wide_long") page + ### only differences: latter has landscape braces and narrower cols + if (tbl_class == "reg_long" | tbl_class == "wide_long") { + # identify number of tables in rda + load(fs::path(tables_dir, "tables", tab)) + # split_tables <- length(table_list) + # identify number of tables that each split table must be further split + # into, with different rows per table + split_table_rows <- length(rda[[1]]$`_data`[[1]]) + split_tables_rowwise <- ceiling(split_table_rows/max_rows) + + # prepare text for chunk that will display split tables + tables_doc_plot_setup2 <- "" + for (i in 1:as.numeric(split_tables_rowwise)) { + # add a chunk for each table + tables_doc_plot_setup2 <- paste0( + tables_doc_plot_setup2, + # add landscape braces before R chunk if tbl_class == "wide_long" + ifelse(tbl_class == "wide_long", + "::: {.landscape}\n\n", + ""), + add_chunk( + paste0( + "# plot table ", i, "\n", + tab_shortname, "_table |>\n", + " gt::tab_options(\n", + " table.width = pct(100),\n", + " table.layout = 'auto'\n", + " ) |>\n", + " gt::cols_width(\n", + " everything() ~ pct(20)\n", + " ) |> \n", + " gt::gt_split(row_every_n = ", max_rows, ") |>\n", + " gt::grp_pull(", i, ")\n" + ), + label = glue::glue("tbl-{tab_shortname}", i), + add_option = TRUE, + chunk_option = c( + "echo: false", + glue::glue( + "tbl-cap: !expr paste0({tab_shortname}_cap, ' ({i} of {split_tables_rowwise})')" + ), + "tbl-pos: 't'" + ) + ), + # add landscape braces after R chunk if tbl_class == "wide_long" + ifelse(tbl_class == "wide_long", + ":::\n", + "\n") + ) + } + } + + ## add table if it is wide enough to be rotated and shown on >1 landscape + if (tbl_class == "ewide_reg") { if (split) { # identify number of split tables load(fs::path(tables_dir, "tables", tab)) split_tables <- length(table_list) } else { - # split extra_wide tables into smaller tables and export AND + # split extra-wide tables into smaller tables and export AND # identify number of split tables IF not already split split_tables <- export_split_tbls( tables_dir = tables_dir, plot_name = tab, essential_columns = 1 ) + + # identify number of split tables + tab <- gsub("table", "table_split", tab) + load(fs::path(tables_dir, "tables", tab)) + split_tables <- length(table_list) } - + # add a chunk to import split tables tables_doc_plot_setup2_import <- paste0( add_chunk( @@ -240,15 +339,23 @@ load(file.path(tables_dir, '", stringr::str_remove(tab, "_split"), "'))\n add_chunk( paste0( "# plot split table ", i, "\n", - tab_shortname, "_table_split_rda[[", i, "]] |> flextable::fit_to_width(max_width = 8)\n" + tab_shortname, "_table_split_rda[[", i, "]] |>\n", + " gt::tab_options(\n", + " table.width = pct(100),\n", + " table.layout = 'auto'\n", + " ) |>\n", + " gt::cols_width(\n", + " everything() ~ pct(20)\n", + " ) \n" ), label = glue::glue("tbl-{tab_shortname}", i), add_option = TRUE, chunk_option = c( "echo: false", glue::glue( - "tbl-cap: !expr paste0({tab_shortname}_cap, '(', {tab_shortname}_cap_split[[", i, "]], ')')" - ) + "tbl-cap: !expr paste0({tab_shortname}_cap, ' ({i} of {split_tables})')" + ), + "tbl-pos: 't'" ) ), "\n", @@ -256,13 +363,104 @@ load(file.path(tables_dir, '", stringr::str_remove(tab, "_split"), "'))\n ":::\n" ) } - + tables_doc_plot_setup2 <- paste0( tables_doc_plot_setup2_import, tables_doc_plot_setup2_display ) } + + ## add table if it is wide and long enough to be rotated and split across >1 landscape pages + if (tbl_class == "ewide_long"){ + if (split) { + # identify number of split tables + load(fs::path(tables_dir, "tables", tab)) + split_tables <- length(table_list) + } else { + # split extra-wide tables into smaller tables and export AND + # identify number of split tables IF not already split + split_tables <- export_split_tbls( + tables_dir = tables_dir, + plot_name = tab, + essential_columns = 1 + ) + + # identify number of split tables + tab <- gsub("table", "table_split", tab) + load(fs::path(tables_dir, "tables", tab)) + split_tables <- length(table_list) + } + # identify number of tables that each split table must be further split + # into, with different rows per table + split_table_rows <- length(table_list[[1]]$`_data`[[1]]) + split_tables_rowwise <- ceiling(split_table_rows/max_rows) + # add a chunk to import split tables + tables_doc_plot_setup2_import <- paste0( + add_chunk( + paste0( + "load(file.path(tables_dir, '", tab, "'))\n +# save rda with plot-specific name\n", + tab_shortname, "_table_split_rda <- table_list\n +# extract table caption specifiers\n", + tab_shortname, "_cap_split <- names(", tab_shortname, "_table_split_rda)" + ), + label = glue::glue("tbl-{tab_shortname}-labels"), + # add_option = TRUE, + chunk_option = c( + "echo: false", + "warnings: false", + glue::glue("include: false") + ) + ), + "\n" + ) + # prepare text for chunk that will display split tables + tables_doc_plot_setup2_display <- "" + for (i in 1:as.numeric(split_tables)) { + for (j in 1:as.numeric(split_tables_rowwise)) { + # add a chunk for each table + tables_doc_plot_setup2_display <- paste0( + tables_doc_plot_setup2_display, + # add landscape braces before R chunk + "::: {.landscape}\n\n", + add_chunk( + paste0( + "# plot split table ", i, "\n", + tab_shortname, "_table_split_rda[[", i, "]] |>\n", + " gt::tab_options(\n", + " table.width = pct(100),\n", + " table.layout = 'auto'\n", + " ) |>\n", + " gt::cols_width(\n", + " everything() ~ pct(20)\n", + " ) |> \n", + " gt::gt_split(row_every_n = ", max_rows, ") |>\n", + " gt::grp_pull(", j, ")\n" + ), + label = glue::glue("tbl-{tab_shortname}", i, "-", j), + add_option = TRUE, + chunk_option = c( + "echo: false", + glue::glue( + "tbl-cap: !expr paste0({tab_shortname}_cap, ' ({i} of {split_tables} tables split by column, {j} of {split_tables_rowwise} tables split by rows)')" + ), + "tbl-pos: 't'" + ) + ), + "\n", + # add landscape braces after R chunk + ":::\n" + ) + } + } + + tables_doc_plot_setup2 <- paste0( + tables_doc_plot_setup2_import, + tables_doc_plot_setup2_display + ) + } + paste0( tables_doc_plot_setup1, tables_doc_plot_setup2 diff --git a/R/export_split_tbls.R b/R/export_split_tbls.R index ad28b228..922b153d 100644 --- a/R/export_split_tbls.R +++ b/R/export_split_tbls.R @@ -32,7 +32,7 @@ export_split_tbls <- function( # split tables and export render_lg_table( - report_flextable = table_rda$table, + report_gt = table_rda$table, essential_columns = essential_columns, tables_dir = tables_dir, plot_name = plot_name diff --git a/R/render_lg_table.R b/R/render_lg_table.R index f1aa622a..08568251 100644 --- a/R/render_lg_table.R +++ b/R/render_lg_table.R @@ -1,9 +1,9 @@ #' Split an extra-wide table into multiple tables #' -#' @param report_flextable The extra-wide flextable. +#' @param report_gt The extra-wide gt table. #' @param essential_columns The columns that will be retained between the split #' tables, formatted as a sequence (e.g., 1:2 for columns 1-2, or 1 for a single -#' column. Example: for the indices table, this could be the year column. +#' column). Example: for the indices table, this could be the year column. #' @param plot_name Name of the .rda file containing the table #' @inheritParams create_tables_doc #' @@ -14,174 +14,111 @@ #' @examples #' \dontrun{ #' render_lg_table( -#' report_flextable = indices_table, +#' report_gt = indices_table, #' essential_columns = 1, #' tables_dir = here::here(), #' plot_name = "indices.abundance_table.rda" #' ) #' #' render_lg_table( -#' report_flextable = important_table, +#' report_gt = important_table, #' essential_columns = 1:3, #' tables_dir = "data", #' plot_name = "bnc_table.rda" #' ) #' } -render_lg_table <- function(report_flextable, +render_lg_table <- function(report_gt, essential_columns, tables_dir, plot_name) { - # calculate key numbers - - # total columns, width of table - total_cols <- flextable::ncol_keys(report_flextable) - total_width <- flextable::flextable_dim(report_flextable)[["widths"]] - - # goal width of each table split from report_flextable, in inches - goal_width <- 7.5 # max is 8" - - # approx width of each column in report_flextable - approx_col_width <- total_width / total_cols - - # goal number of columns per table split from report_flextable - goal_cols_per_table <- ceiling(goal_width / approx_col_width) - - # split tables needed - num_tables <- ceiling(total_cols / goal_cols_per_table) - - # set up main df organizing split table information - table_cols <- matrix(NA, - nrow = num_tables, - ncol = 4 - ) |> - as.data.frame() |> - dplyr::rename( - "table" = 1, - "cols_to_keep" = 2, - "start_col" = 3, - "end_col" = 4 - ) - - i <- 1 - # TODO: add error improved error message - essential_cols <- essential_columns - for (i in 1:num_tables) { - # set table number to i - table_num <- i - - # add table number to df - table_cols$table[i] <- i - - # get first and last columns of new table - if (i == 1) { - init_col <- 1 - end_col <- init_col + goal_cols_per_table - } else if ((i < num_tables) & (init_col < total_cols)) { - # subtracting essential_cols so the first cols will be the essential ones - # and the total cols will still = goal_cols_per_table - end_col <- init_col + goal_cols_per_table - length(essential_cols) + + # Clean up essential_columns + is_empty_essentials <- is.null(essential_columns) || + length(essential_columns) == 0 || + (length(essential_columns) == 1 && essential_columns == "0") + + if (is_empty_essentials) { + essential_cols_cleaned <- character(0) + } else { + # If numeric (like 1:2), convert to actual column names to prevent index shifting errors + if (is.numeric(essential_columns)) { + essential_cols_cleaned <- colnames(report_gt[["_data"]])[essential_columns] } else { - end_col <- total_cols + essential_cols_cleaned <- essential_columns } - - table_cols$cols_to_keep[i] <- paste0(init_col, ":", end_col) - - table_cols$start_col[i] <- init_col - table_cols$end_col[i] <- end_col - - # add goal_cols_per_table for next table - init_col <- end_col + 1 - - # add 1 for next table - i <- i + 1 } - - # add col with essential cols - table_cols <- table_cols |> - dplyr::mutate( - essential_cols = paste(essential_cols, collapse = ", "), - - # find cols to delete - cols_to_del = apply(table_cols, 1, function(row) { - curr_range <- row["start_col"]:row["end_col"] - miss_nums <- setdiff(1:total_cols, curr_range) - paste(miss_nums, collapse = ", ") - }), - # make cols_to_delete and essential_cols into sequences - cols_to_del_seq = lapply(cols_to_del, function(x) as.numeric(unlist(strsplit(x, ",")))), - essential_cols_seq = lapply(essential_cols, function(x) as.numeric(unlist(strsplit(x, ",")))), - # find the final columns to delete by removing essential_cols from - # cols_to_delete - final_cols_to_del = mapply(function(seq1, seq2) { - paste(setdiff(seq1, seq2), collapse = ", ") - }, cols_to_del_seq, essential_cols_seq) - ) - - # print all split tables by removing final_cols_to_del from report_flextable - # for (i in 1:num_tables) { - # report_flextable |> - # flextable::delete_columns(j = c( - # as.numeric( - # unlist( - # strsplit( - # table_cols[i,"final_cols_to_del"], ",") - # ) - # ) - # ) - # ) |> - # print() - # } - - # save all tables to a list + + col_inches <- 1.5 + + # calculate key numbers + # total columns, width of table in inches + total_cols <- ncol(report_gt[["_data"]]) + total_width <- total_cols * col_inches + + # goal width of each table split from report_gt, in inches + goal_width <- 7.5 # max is 8" + + # goal number of columns per table split from report_gt + goal_cols_per_table <- ceiling(goal_width / col_inches) + + all_cols <- colnames(report_gt[["_data"]]) + moving_cols <- setdiff(all_cols, essential_cols_cleaned) + + total_slots <- floor(goal_width / col_inches) + + # Slots for non-essential columns + non_ess_slots <- max(1, total_slots - length(essential_cols_cleaned)) + + # Chunk the moving columns + col_chunks <- split(moving_cols, + ceiling(seq_along(moving_cols) / non_ess_slots)) + + # Loop through chunks and build tables table_list <- list() - for (i in 1:num_tables) { - split_table <- report_flextable |> - flextable::delete_columns(j = c( - as.numeric( - unlist( - strsplit( - table_cols[i, "final_cols_to_del"], "," - ) - ) - ) - )) |> - flextable::fit_to_width(max_width = goal_width) |> - flextable::hline( - part = "header", - # i = 1, - border = officer::fp_border( - width = 1.5, - color = "#666666" - ) + for (i in seq_along(col_chunks)) { + + current_keep <- c(essential_cols_cleaned, col_chunks[[i]]) + + # Create the gt objects + split_table <- report_gt |> + gt::cols_hide(columns = -dplyr::all_of(current_keep)) |> + gt::tab_options(table.width = gt::px(goal_width * 96)) |> + gt::tab_style( + style = gt::cell_borders(sides = "bottom", + color = "#666666", + weight = gt::px(1.5)), + locations = gt::cells_column_labels() ) |> - flextable::valign(valign = "center", part = "header") - + gt::tab_style( + style = gt::cell_text(v_align = "middle"), + locations = gt::cells_column_labels() + ) + + # Store table in list table_list[[i]] <- split_table - - # get rownames of split table - all_vals <- split_table$header$dataset - shown_vals <- c(split_table[["header"]][["content"]][["keys"]]) - split_tbl_vals <- all_vals |> - dplyr::select(dplyr::intersect( - names(all_vals), - shown_vals - )) |> - dplyr::slice(1) |> - dplyr::select_if(~ !(all(is.na(.)) | all(. == ""))) |> - as.character() |> - unique() |> - stringr::str_squish() |> - paste(collapse = ", ") - - # add rownames to table_list - names(table_list)[[i]] <- split_tbl_vals - - - # if(i == num_tables){ - # single_tab <- table_list[[returned_tab]] - # return(single_tab) - # } + + # Get labels for visible columns only + all_vals <- split_table[["_boxhead"]] + + if (!is.null(all_vals)) { + display_names <- all_vals |> + dplyr::filter(type != "hidden") |> + dplyr::pull(column_label) |> + as.character() |> + (\(x) x[!is.na(x) & x != ""])() |> + unique() |> + stringr::str_squish() |> + paste(collapse = ", ") + + # Assign name to list index + if (nchar(display_names) > 0) { + names(table_list)[i] <- display_names + } else { + names(table_list)[i] <- paste0("Table_Part_", i) + } + } } + # save table_list as rda save(table_list, file = fs::path( @@ -191,5 +128,5 @@ render_lg_table <- function(report_flextable, ) ) - num_tables + length(table_list) } diff --git a/README.md b/README.md index 5877e2ec..4917d24e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ asar::create_template( species = "Petrale sole", spp_latin = "Eopsetta jordani", year = 2023, - author = c("Ian G. Taylor"="NWFSC", "Vladlena Gertseva"="NWFSC", "Nick Tolimieri"="NWFSC"), + authors = c("Ian G. Taylor"="NWFSC", "Vladlena Gertseva"="NWFSC", "Nick Tolimieri"="NWFSC"), include_affiliation = TRUE, simple_affiliation = FALSE, param_names = c("nf","sf"), diff --git a/man/ID_tbl_length_class.Rd b/man/ID_tbl_length_class.Rd new file mode 100644 index 00000000..8ff82d41 --- /dev/null +++ b/man/ID_tbl_length_class.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ID_tbl_length_class.R +\name{ID_tbl_length_class} +\alias{ID_tbl_length_class} +\title{Identify table length class} +\usage{ +ID_tbl_length_class(tables_dir, plot_name) +} +\arguments{ +\item{tables_dir}{The location of the "tables" folder, which contains tables +files.} + +\item{plot_name}{Name of the .rda file containing the table} +} +\value{ +The length class of a table: regular or long. The result +will determine whether the table can be rendered on a page +as-is, or if it needs to be split across multiple pages. +} +\description{ +Identify table length class +} +\examples{ +\dontrun{ +ID_tbl_length_class( + plot_name = "indices.abundance_table.rda", + tables_dir = here::here() +) +} +} diff --git a/man/ID_tbl_width_class.Rd b/man/ID_tbl_width_class.Rd index 35de7dd8..da234fdb 100644 --- a/man/ID_tbl_width_class.Rd +++ b/man/ID_tbl_width_class.Rd @@ -19,7 +19,7 @@ be resized, rotated, and/or split across multiple pages.} } \value{ The width class of a table: regular, wide, or extra-wide. The result -will determine whether the table that can be rendered on a portrait page +will determine whether the table can be rendered on a portrait page as-is, or if it needs to be resized, rotated, and/or split across multiple pages. } \description{ diff --git a/man/export_split_tbls.Rd b/man/export_split_tbls.Rd index dcd184f7..245bfa7f 100644 --- a/man/export_split_tbls.Rd +++ b/man/export_split_tbls.Rd @@ -18,7 +18,7 @@ files.} \item{essential_columns}{The columns that will be retained between the split tables, formatted as a sequence (e.g., 1:2 for columns 1-2, or 1 for a single -column. Example: for the indices table, this could be the year column.} +column). Example: for the indices table, this could be the year column.} } \value{ The number of split tables diff --git a/man/render_lg_table.Rd b/man/render_lg_table.Rd index 8b4229e2..d8f39174 100644 --- a/man/render_lg_table.Rd +++ b/man/render_lg_table.Rd @@ -4,14 +4,14 @@ \alias{render_lg_table} \title{Split an extra-wide table into multiple tables} \usage{ -render_lg_table(report_flextable, essential_columns, tables_dir, plot_name) +render_lg_table(report_gt, essential_columns, tables_dir, plot_name) } \arguments{ -\item{report_flextable}{The extra-wide flextable.} +\item{report_gt}{The extra-wide gt table.} \item{essential_columns}{The columns that will be retained between the split tables, formatted as a sequence (e.g., 1:2 for columns 1-2, or 1 for a single -column. Example: for the indices table, this could be the year column.} +column). Example: for the indices table, this could be the year column.} \item{tables_dir}{The location of the "tables" folder, which contains tables files.} @@ -27,14 +27,14 @@ Split an extra-wide table into multiple tables \examples{ \dontrun{ render_lg_table( - report_flextable = indices_table, + report_gt = indices_table, essential_columns = 1, tables_dir = here::here(), plot_name = "indices.abundance_table.rda" ) render_lg_table( - report_flextable = important_table, + report_gt = important_table, essential_columns = 1:3, tables_dir = "data", plot_name = "bnc_table.rda" diff --git a/tests/testthat/test-ID_tbl_length_class.R b/tests/testthat/test-ID_tbl_length_class.R new file mode 100644 index 00000000..362a4ff8 --- /dev/null +++ b/tests/testthat/test-ID_tbl_length_class.R @@ -0,0 +1,103 @@ +test_that("Table length calculated correctly for regular table", { + # make sample dataset + library(gt) + test_table <- data.frame( + year = rep(seq(1987, 2000, 1), times = 3), + estimate = rep(rnorm(42, mean = 1000, sd = 100)), + label = rep(c("spawning_biomass", "catch", "abundance"), each = 14) + ) |> + tidyr::pivot_wider(id_cols = year, names_from = label, values_from = estimate) |> + gt() + + rda <- list( + "table" = test_table, + "caption" = "test_caption" + ) + + tbl_path <- fs::path(getwd(), "tables") + dir.create(tbl_path) + save(rda, file = fs::path(getwd(), "tables", "indices.abundance_table.rda")) + + # regular length + tbl_width <- ID_tbl_length_class( + plot_name = "indices.abundance", + tables_dir = getwd() + ) + + expected_output <- "regular" + expect_equal(tbl_width, expected_output) + + # erase temporary testing files + unlink(tbl_path, recursive = T) +}) + +test_that("Table lengths calculated correctly for a long table", { + # make sample dataset + library(gt) + test_table <- data.frame( + species = sample(c("Tuna", "Cod", "Trout", "Salmon"), 20, replace = TRUE), + location = c( + "Providence_Rhode_Island", + "Narragansett_Bay", + "Point_Judith_Pond", + "Block_Island_Sound", + "Cape_Cod_Bay", + "Georges_Bank", + "Gulf_of_Maine", + "Montauk_New_York", + "Hudson_Canyon", + "Long_Island_Sound", + "Chesapeake_Bay", + "Outer_Banks_NC", + "Florida_Keys", + "Gulf_of_Mexico", + "Monterey_Bay_CA", + "Puget_Sound_WA", + "Kodiak_Alaska", + "Bering_Sea", + "Hawaiian_Islands", + "Sargasso_Sea", + "Buzzards_Bay_MA", + "Stellwagen_Bank", + "Vineyard_Sound", + "Great_South_Channel", + "Casco_Bay_Maine", + "Grand_Banks", + "Scotian_Shelf", + "Delaware_Bay", + "Pamlico_Sound_NC", + "Charleston_Bump_SC", + "Gray_Reef_GA", + "St_Johns_River_FL", + "Mobile_Bay_AL", + "Flower_Garden_Banks", + "Channel_Islands_CA", + "Columbia_River_Estuary", + "Gulf_of_the_Farallones", + "Aleutian_Islands", + "Chukchi_Sea", + "Beaufort_Sea" + ) + ) |> + gt() + + rda <- list( + "table" = test_table, + "caption" = "test_caption" + ) + + tbl_path <- fs::path(getwd(), "tables") + dir.create(tbl_path) + save(rda, file = fs::path(getwd(), "tables", "bnc_table.rda")) + + tbl_width2 <- ID_tbl_length_class( + plot_name = "bnc", + tables_dir = getwd() + ) + + expected_output <- "long" + expect_equal(tbl_width2, expected_output) + + # erase temporary testing files + unlink(tbl_path, recursive = T) +}) diff --git a/tests/testthat/test-ID_tbl_width_class.R b/tests/testthat/test-ID_tbl_width_class.R index 8e15f8c1..86d3e19e 100644 --- a/tests/testthat/test-ID_tbl_width_class.R +++ b/tests/testthat/test-ID_tbl_width_class.R @@ -1,13 +1,13 @@ test_that("Table widths calculated correctly for wide table", { # make sample dataset - library(flextable) + library(gt) test_table <- data.frame( year = rep(seq(1987, 2024, 1), times = 3), estimate = rep(rnorm(38, mean = 1000, sd = 100), times = 6), label = rep(c("spawning_biomass", "catch", "abundance", "new_col1", "new_col2", "new_col3"), each = 38) ) |> tidyr::pivot_wider(id_cols = year, names_from = label, values_from = estimate) |> - flextable() + gt() rda <- list( "table" = test_table, @@ -34,7 +34,7 @@ test_that("Table widths calculated correctly for wide table", { test_that("Table widths calculated correctly for extra-wide table", { # make sample dataset - library(flextable) + library(gt) test_table <- data.frame( species = sample(c("Tuna", "Cod", "Trout", "Salmon"), 20, replace = TRUE), location = c( @@ -62,7 +62,7 @@ test_that("Table widths calculated correctly for extra-wide table", { is_tagged = sample(c(TRUE, FALSE), 20, replace = TRUE) ) |> tidyr::pivot_wider(id_cols = species, names_from = location, values_from = is_tagged) |> - flextable() + gt() rda <- list( "table" = test_table, diff --git a/tests/testthat/test-create_tables_doc.R b/tests/testthat/test-create_tables_doc.R index 9bdbbcde..bddebda9 100644 --- a/tests/testthat/test-create_tables_doc.R +++ b/tests/testthat/test-create_tables_doc.R @@ -27,117 +27,117 @@ test_that("Creates expected start of nearly empty tables doc", { file.remove(fs::path(getwd(), "08_tables.qmd")) }) -# test_that("Creates expected start of tables doc with table", { -# # load sample dataset -# load(file.path( -# "fixtures", "ss3_models_converted", "Hake_2018", -# "std_output.rda" -# )) -# -# stockplotr::table_landings(out_new, -# make_rda = TRUE -# ) -# -# # create tables doc -# create_tables_doc( -# subdir = getwd(), -# tables_dir = getwd() -# ) -# -# # read in tables doc -# table_content <- readLines("08_tables.qmd") -# # extract first 7 lines -# head_table_content <- head(table_content, 7) -# # remove line numbers and collapse -# fc_pasted <- paste(head_table_content, collapse = "") -# -# # expected tables doc head -# expected_head_table_content <- "# Tables {#sec-tables} ```{r} #| label: 'set-rda-dir-tbls'#| echo: false #| warning: false #| include: false" -# -# # test expectation of start of tables doc -# testthat::expect_equal( -# fc_pasted, -# expected_head_table_content -# ) -# -# # erase temporary testing files -# file.remove(fs::path(getwd(), "08_tables.qmd")) -# file.remove(fs::path(getwd(), "captions_alt_text.csv")) -# unlink(fs::path(getwd(), "tables"), recursive = T) -# }) - -# test_that("Throws warning if chunks with identical labels", { -# # load sample dataset -# load(file.path( -# "fixtures", "ss3_models_converted", "Hake_2018", -# "std_output.rda" -# )) -# -# stockplotr::table_landings(out_new, -# make_rda = TRUE -# ) -# -# # create tables doc -# create_tables_doc( -# subdir = getwd(), -# tables_dir = getwd() -# ) -# -# expect_message( -# create_tables_doc( -# subdir = getwd(), -# tables_dir = getwd() -# ), -# "Tables doc will not render if chunks have identical labels." -# ) -# -# # erase temporary testing files -# file.remove(fs::path(getwd(), "08_tables.qmd")) -# file.remove(fs::path(getwd(), "captions_alt_text.csv")) -# unlink(fs::path(getwd(), "tables"), recursive = T) -# }) - -# test_that("Formerly empty tables doc renders correctly", { -# # create empty tables doc -# create_template() -# -# # load sample dataset -# load(file.path( -# "fixtures", "ss3_models_converted", "Hake_2018", -# "std_output.rda" -# )) -# -# stockplotr::table_landings( -# dat = out_new, -# make_rda = TRUE, -# module = "TIME_SERIES" -# ) -# -# # rerender tables doc, appending new table -# create_tables_doc( -# subdir = file.path(getwd(), "report"), -# tables_dir = getwd() -# ) -# -# # read in tables doc -# table_content <- readLines(file.path(getwd(), "report", "08_tables.qmd")) -# # extract first 8 lines -# head_table_content <- head(table_content, 8) -# # remove line numbers and collapse -# fc_pasted <- paste(head_table_content, collapse = "") -# -# # expected tables doc head -# expected_head_table_content <- "# Tables {#sec-tables} ```{r} #| label: 'set-rda-dir-figs'#| echo: false #| warnings: false #| eval: true" -# -# # test expectation of start of tables doc -# expect_equal( -# fc_pasted, -# expected_head_table_content -# ) -# -# # erase temporary testing files -# file.remove(fs::path(getwd(), "08_tables.qmd")) -# file.remove(fs::path(getwd(), "captions_alt_text.csv")) -# unlink(fs::path(getwd(), "tables"), recursive = T) -# unlink(fs::path(getwd(), "report"), recursive = T) -# }) +test_that("Creates expected start of tables doc with table", { + # load sample dataset + load(file.path( + "fixtures", "ss3_models_converted", "Hake_2018", + "std_output.rda" + )) + + stockplotr::table_landings(out_new, + make_rda = TRUE + ) + + # create tables doc + create_tables_doc( + subdir = getwd(), + tables_dir = getwd() + ) + + # read in tables doc + table_content <- readLines("08_tables.qmd") + # extract first 7 lines + head_table_content <- head(table_content, 7) + # remove line numbers and collapse + fc_pasted <- paste(head_table_content, collapse = "") + + # expected tables doc head + expected_head_table_content <- "# Tables {#sec-tables} ```{r} #| label: 'set-rda-dir-tbls'#| echo: false #| warning: false #| include: false" + + # test expectation of start of tables doc + testthat::expect_equal( + fc_pasted, + expected_head_table_content + ) + + # erase temporary testing files + file.remove(fs::path(getwd(), "08_tables.qmd")) + file.remove(fs::path(getwd(), "captions_alt_text.csv")) + unlink(fs::path(getwd(), "tables"), recursive = T) +}) + +test_that("Throws warning if chunks with identical labels", { + # load sample dataset + load(file.path( + "fixtures", "ss3_models_converted", "Hake_2018", + "std_output.rda" + )) + + stockplotr::table_landings(out_new, + make_rda = TRUE + ) + + # create tables doc + create_tables_doc( + subdir = getwd(), + tables_dir = getwd() + ) + + expect_message( + create_tables_doc( + subdir = getwd(), + tables_dir = getwd() + ), + "Tables doc will not render if chunks have identical labels." + ) + + # erase temporary testing files + file.remove(fs::path(getwd(), "08_tables.qmd")) + file.remove(fs::path(getwd(), "captions_alt_text.csv")) + unlink(fs::path(getwd(), "tables"), recursive = T) +}) + +test_that("Formerly empty tables doc renders correctly", { + # create empty tables doc + create_template() + + # load sample dataset + load(file.path( + "fixtures", "ss3_models_converted", "Hake_2018", + "std_output.rda" + )) + + stockplotr::table_landings( + dat = out_new, + make_rda = TRUE, + module = "CATCH" + ) + + # rerender tables doc, appending new table + create_tables_doc( + subdir = file.path(getwd(), "report"), + tables_dir = getwd() + ) + + # read in tables doc + table_content <- readLines(file.path(getwd(), "report", "08_tables.qmd")) + # extract first 8 lines + head_table_content <- head(table_content, 8) + # remove line numbers and collapse + fc_pasted <- paste(head_table_content, collapse = "") + + # expected tables doc head + expected_head_table_content <- "# Tables {#sec-tables} ```{r} #| label: 'set-rda-dir-tbls'#| echo: false #| warning: false #| include: false" + + # test expectation of start of tables doc + expect_equal( + fc_pasted, + expected_head_table_content + ) + + # erase temporary testing files + file.remove(fs::path(getwd(), "08_tables.qmd")) + file.remove(fs::path(getwd(), "captions_alt_text.csv")) + unlink(fs::path(getwd(), "tables"), recursive = T) + unlink(fs::path(getwd(), "report"), recursive = T) +}) diff --git a/tests/testthat/test-export_split_tbls.R b/tests/testthat/test-export_split_tbls.R index 47987b62..9e88b106 100644 --- a/tests/testthat/test-export_split_tbls.R +++ b/tests/testthat/test-export_split_tbls.R @@ -1,34 +1,86 @@ -# test_that("Number of split tables is calculated correctly for converted bam model", { -# # read in sample dataset -# dat <- utils::read.csv( -# file = fs::path("fixtures", "bam_models_converted", "bsb_conout.csv") -# ) -# -# # bam model with table split into 2 -# stockplotr::table_indices(dat, -# make_rda = TRUE -# ) -# -# # indices table -# num_tabs <- export_split_tbls( -# tables_dir = getwd(), -# plot_name = "indices.abundance_table.rda", -# essential_columns = 1 -# ) -# -# # expect 2 tables -# expected_output <- 2 -# expect_equal(num_tabs, expected_output) -# -# # expect to see an "indices.abundance_table_split.rda" file -# expect_no_error("indices.abundance_table_split.rda" %in% list.files(file.path("tables"))) -# -# # expect that an object "table_list" imported into environment -# expect_no_error(load(file.path("tables", "indices.abundance_table_split.rda"))) -# -# expect_equal(length(table_list), 2) -# -# # erase temporary testing files -# file.remove(fs::path(getwd(), "captions_alt_text.csv")) -# unlink(fs::path(getwd(), "tables"), recursive = T) -# }) +test_that("Number of split tables is calculated correctly for 1 table", { + library(gt) + load(file.path( + "fixtures", "ss3_models_converted", "Hake_2018", + "std_output.rda" + )) + + stockplotr::table_landings(out_new, + module = "CATCH", + make_rda = TRUE + ) + + + # indices table + num_tabs <- export_split_tbls( + tables_dir = getwd(), + plot_name = "landings_table.rda", + essential_columns = 1 + ) + + # expect 1 table + expected_output <- 1 + expect_equal(num_tabs, expected_output) + + # erase temporary testing files + file.remove(fs::path(getwd(), "captions_alt_text.csv")) + unlink(fs::path(getwd(), "tables"), recursive = T) +}) + + +test_that("Number of split tables is calculated correctly for 3 tables", { + library(gt) + load(file.path( + "fixtures", "ss3_models_converted", "Hake_2018", + "std_output.rda" + )) + + stockplotr::table_landings(out_new, + module = "CATCH", + make_rda = TRUE + ) + + load(file.path("tables", "landings_table.rda")) + + wider_df <- cbind( + rda$table$`_data`, + data.frame( + Col1 = 1:52, + Col2 = 52:103, + Col3 = 104:155, + Col4 = 156:207, + Col5 = 208:259, + Col6 = 260:311, + Col7 = 312:363, + Col8 = 364:415 + ) + ) |> + gt() + + rda$table <- wider_df + save(rda, file = file.path(getwd(), "tables", "landings_table.rda")) + + # indices table + num_tabs <- export_split_tbls( + tables_dir = getwd(), + plot_name = "landings_table.rda", + essential_columns = 1 + ) + + # expect 3 tables + expected_output <- 3 + expect_equal(num_tabs, expected_output) + + # expect to see an "landings_table_split.rda" file + expect_no_error("landings_table_split.rda" %in% list.files(file.path("tables"))) + + # expect that an object "table_list" imported into environment + expect_no_error(load(file.path("tables", "landings_table_split.rda"))) + + expect_equal(length(table_list), 3) + + # erase temporary testing files + file.remove(fs::path(getwd(), "captions_alt_text.csv")) + unlink(fs::path(getwd(), "tables"), recursive = T) + +}) diff --git a/tests/testthat/test-render_lg_table.R b/tests/testthat/test-render_lg_table.R index d4b799e2..a7fad84e 100644 --- a/tests/testthat/test-render_lg_table.R +++ b/tests/testthat/test-render_lg_table.R @@ -1,30 +1,33 @@ test_that("accurate number of split tables identified", { + library(gt) # make very wide table ft <- as.data.frame(faithful) |> t() |> as.data.frame() |> dplyr::select(1:50) |> - flextable::flextable() + gt::gt() dir.create("tables") # 0 essential columns - tables_test1 <- render_lg_table(ft, + tables_test1 <- render_lg_table( + report_gt = ft, essential_columns = 0, tables_dir = getwd(), plot_name = "test_plot1.rda" ) - expect_equal(tables_test1, 5) + expect_equal(tables_test1, 10) # 2 essential columns - tables_test2 <- render_lg_table(ft, + tables_test2 <- render_lg_table( + report_gt = ft, essential_columns = 1:2, tables_dir = getwd(), plot_name = "test_plot2.rda" ) - expect_equal(tables_test2, 5) + expect_equal(tables_test2, 16) # make very very wide table @@ -32,7 +35,7 @@ test_that("accurate number of split tables identified", { t() |> as.data.frame() |> dplyr::select(1:70) |> - flextable::flextable() + gt::gt() # 0 essential columns tables_test3 <- render_lg_table(ft, @@ -41,7 +44,7 @@ test_that("accurate number of split tables identified", { plot_name = "test_plot3.rda" ) - expect_equal(tables_test3, 7) + expect_equal(tables_test3, 14) # 3 essential columns tables_test4 <- render_lg_table(ft, @@ -50,7 +53,7 @@ test_that("accurate number of split tables identified", { plot_name = "test_plot4.rda" ) - expect_equal(tables_test4, 7) + expect_equal(tables_test4, 34) # make slightly wide table @@ -58,7 +61,7 @@ test_that("accurate number of split tables identified", { t() |> as.data.frame() |> dplyr::select(1:20) |> - flextable::flextable() + gt::gt() # 0 essential columns tables_test5 <- render_lg_table(ft, @@ -67,7 +70,7 @@ test_that("accurate number of split tables identified", { plot_name = "test_plot5.rda" ) - expect_equal(tables_test5, 2) + expect_equal(tables_test5, 4) # 3 essential columns tables_test6 <- render_lg_table(ft, @@ -76,7 +79,7 @@ test_that("accurate number of split tables identified", { plot_name = "test_plot6.rda" ) - expect_equal(tables_test6, 2) + expect_equal(tables_test6, 9) # erase temporary testing files unlink(fs::path(getwd(), "tables"), recursive = T) @@ -88,7 +91,7 @@ test_that("table_list saved as an rda", { t() |> as.data.frame() |> dplyr::select(1:50) |> - flextable::flextable() + gt::gt() dir.create("tables") diff --git a/vignettes/accessibility_guide.Rmd b/vignettes/accessibility_guide.Rmd index 592d5f1a..ffe5a9cf 100644 --- a/vignettes/accessibility_guide.Rmd +++ b/vignettes/accessibility_guide.Rmd @@ -185,6 +185,8 @@ recruitment_alt_text <- new_alt_text 3. As stated earlier, if you see text that looks like a placeholder (e.g., "The x axis, showing the year, spans from B.start.year to B.end.year..."), that means that there was at least one instance where our tool failed to extract a specific value from the model results and substitute it into the placeholder. Please make sure that your alt text and captions contain the expected values before moving forward with your report. Check out the inst/resources/captions_alt_text_template.csv file in the `stockplotr` package to view the template with placeholders. The same package's `write_captions()` function shows how values are extracted from the model results and substituted into the placeholders. +4. If you add a special character (e.g., percentage sign (%) or dollar sign ($); see [full list on this wiki page](https://en.wikibooks.org/wiki/LaTeX/Special_Characters#Other_symbols)), please add two backslashes before the character to avoid issues compiling your report later on (specifically, the conversion from Quarto to LaTeX via Pandoc, and then compilation of the LaTeX report after running `add_accessibility()` or `add_alttext()`). For example, "The 95% CI" would be written as "The 95\\\\% CI". + ### More resources Looking for more resources for writing alt text? Check out the [NOAA Library's website for creating accessible documents](https://library.noaa.gov/Section508/CreatingDocs).