---
title: "Introduction to {aftables}"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{aftables}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#"
)
```

## Purpose

This vignette demonstrates how to use {aftables} to generate a spreadsheet output that automatically follows [best practice](https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/).

## Installation

Install the latest release version of aftables directly from CRAN:

``` {r install from CRAN, eval=FALSE}
install.packages("aftables")
```

## Workflow

Having installed the {aftables} package, there are three steps to generating a compliant spreadsheet:

1. Use `create_aftable()` to create a special dataframe (with class 'aftable') that contains all the content that will go in your spreadsheet
1. Pass the output to `generate_workbook()` to convert the aftable to {openxlsx2}'s 'wbWorkbook' class, which adds spreadsheet structure and styles
1. Pass the output to `openxlsx2::wb_save()` to write out to an xlsx file (or `openxlsx2::wb_open()` to open a temporary copy)

You can use the package's [RStudio Addin](https://rstudio.github.io/rstudioaddins/), which is installed with {aftables}, to insert a pre-filled demo skeleton of this workflow (RStudio users only).

### 1. Create an aftable

Each argument to `create_aftable()` provides the information needed to construct each sheet in the spreadsheet.

| Argument | Required | Type | Accepted values | Explanation |
| :--- | :- | :--- | :--- | :------ |
| `tab_titles` | Yes | Character vector | | The name that will appear on each sheet's tab in the output spreadsheet |
| `sheet_types` | Yes | Character vector | 'cover', 'contents', 'notes', 'tables' | The kind of information that the sheet holds, which is needed so that the correct structure and formatting can be applied later |
| `sheet_titles` | Yes | Character vector | | The main heading of each sheet, which will appear in cell A1 |
| `blank_cells` | No | Character vector | | A sentence that explains the reason for any blank cells in the sheet (if applicable) |
| `custom_rows` | No | List of character vectors | | Arbitrary rows of text that the user wants to insert above a table, one list-item per sheet (contents, notes and tables sheets), one vector element per row |
| `sources` | No | Character vector | | A sentence provides the source of the data found in each table (if applicable, likely only needed for sheets with `sheet_types` of 'table') |
| `tables` | Yes | List of dataframes (although the cover sheet content can be provided as a list object) | | The main content for each sheet, expressed as flat ([probably tidy](https://www.jstatsoft.org/article/view/v059i10)) dataframes of rows and columns (though the cover can be a list) |

You can read more about these arguments and their requirements in the function's help pages, which you can access by running `?create_aftable` in the R console. See also the [terminology vignette](https://best-practice-and-impact.github.io/aftables/articles/terminology), `vignette("terminology", "aftables")`, for these terms and more.

#### Pre-prepare tables

Rather than pass a big list of dataframes directly to the `tables` argument of `create_aftable()`, it's preferable to prepare them first into their own named objects.

Below are some demo tables that we will later pass to `create_aftable()`. I've used `tibble::tribble()` for human-readable row-by-row dataframe construction, but you can just use `data.frame()` if you want.

##### Meta-sheets

The cover can accept either a list or a data.frame (the latter was the only acceptable input prior to version 0.2.0). We recommend a list so that you can have multiple rows per section on the cover. This also means you can dedicate certain rows to be hyperlinks to web URLs or mailto links that will open an email client. Here's a demo list for the contents page (required):

```{r tables-cover-list}
cover_list <- list(
  "Section 1" = c("First row of Section 1.", "Second row of Section 1."),
  "Section 2" = "The only row of Section 2.",
  "Section 3" = c(
    "[Website](https://best-practice-and-impact.github.io/aftables/)",
    "[Email address](mailto:fake.address@aftables.com)"
  )
)
```

Note: a list is the preferred method of input for the cover. Previously, a data.frame was the only way to supply the data for the cover sheet in version 0.1 of the package.

Here's a demo table for the contents page (required):

```{r tables-contents}
contents_df <- data.frame(
  "Sheet name" = c("Notes", "Table_1", "Table_2"),
  "Sheet title" = c(
    "Notes used in this workbook",
    "First Example Sheet",
    "Second Example Sheet"
  ),
  check.names = FALSE
)
```

And here's a demo table for the notes page (not required if there's no notes in your tables), which has a column for the note number in the form '[note x]' and a column for the note itself:

```{r tables-notes}
notes_df <- data.frame(
  "Note number" = paste0("[note ", 1:3, "]"),
  "Note text" = c("First note.", "Second note.", "Third note."),
  check.names = FALSE
)
```

<details><summary>Click to preview these objects</summary>
```{r tables-meta-expand}
cover_list
contents_df
notes_df
```
</details>

##### Statistical tables

The code below generates a demo data.frame that we're going to pretend is the statistical data that we want to publish. It has columns with different sorts of data that we might want to publish. It also has suppressed values (e.g. '[c]' meaning 'confidential' data) and includes notes (in the form '[note x]').

```{r stats-df-1}
table_1_df <- data.frame(
  Category = LETTERS[1:10],
  "Numeric [note 1]" = 1:10,
  "Numeric suppressed" = c(1:4, "[c]", 6:9, "[x]"),
  "Numeric thousands" = abs(round(rnorm(10), 4) * 1e5),
  "Numeric decimal" = abs(round(rnorm(10), 5)),
  "Long name that means that the column width needs to be widened" = 1:10,
  Notes = c("[note 1]", rep(NA_character_, 4), "[note 2]", rep(NA_character_, 4)),
  check.names = FALSE
)
```

We'll create a second, simpler table as well, which will go on a separate sheet:

```{r stats-df-2}
table_2_df <- data.frame(Category = LETTERS[1:10], Numeric = 1:10)
```

<details><summary>Click to preview these tables of statistical data</summary>
```{r table-stat-expand}
table_1_df
table_2_df
```
</details>

See [the best practice guidance](https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/) for more information on how to present data in these tables.

#### Optional step: setting number formats with number_formatter function
The default behaviour of aftables is to format numbers with thousand separators, and with the number of decimal places determined from the source data. In the example table `table_1_df` the columns `Numeric [note 1]`, `Numeric suppressed`, `Numeric thousands`, `Numeric decimal`, and `Long name that means that the column width needs to be widened` will be formatted with thousand separators. The `Numeric decimal` column will be formatted with 5 decimal places; the level of precision required to display all the figures after the decimal point and keep the decimal points aligned across table rows.

The number_formatter function can be used to override aftables' default behaviour and specify if thousand separators should be used, and how many decimal places should be displayed. This code changes the number of decimal places for the `Numeric decimal` column from 5 (by default) to 2, and removes thousand separators for the `Numeric thousands` column:

```{r results=FALSE}
table_1_df <- table_1_df |>
  aftables::number_formatter(
    columns = "Numeric decimal",
    decimal_places = 2
  ) |>
  aftables::number_formatter(
    columns = "Numeric thousands",
    thousand_separators = FALSE
  )
```


These number formats do not change the source data, and so are not visible when viewing the table in R. The number formats are applied when the data tables are added to the Excel workbook object by the `generate_workbook()` workbook.

#### Create aftable

Now we can construct an aftable by passing the required sheet elements as character vectors with `c()`—or a `list()` in the case of the `tables` and `custom_rows` arguments—to the `create_aftable()` function. 

Note that:

* the element index of the object supplied to each argument is the sheet that it will be applied to (e.g. the tab title of the first sheet will be 'Cover', the sheet type of the second sheet will be 'contents' and the fourth sheet will contain the `table_1_df` table)
* you must use `NA_character_` wherever an element isn't required (e.g. there is no information about blank cells nor sources for the first three sheets)
* you can insert a template of this demo using the package's RStudio Addin

```{r new-aftable}
my_aftable <- aftables::create_aftable(
  tab_titles = c("Cover", "Contents", "Notes", "Table 1", "Table_2"),
  sheet_types = c("cover", "contents", "notes", "tables", "tables"),
  sheet_titles = c(
    "The 'aftables' Demo Workbook",
    "Table of contents",
    "Notes",
    "Table 1: First Example Sheet",
    "Table 2: Second Example Sheet"
  ),
  blank_cells = c(
    rep(NA_character_, 3),
    "Blank cells indicate that there's no note in that row.",
    NA_character_
  ),
  custom_rows = list(
    NA_character_,
    NA_character_,
    "A custom row.",
    c(
      "First custom row [with a hyperlink.](https://best-practice-and-impact.github.io/aftables/)",
      "Second custom row."
    ),
    "A custom row."
  ),
  sources = c(
    rep(NA_character_, 3),
    "[The Source Material., 2024](https://best-practice-and-impact.github.io/aftables/)",
    "The Source Material, 2024."
  ),
  tables = list(cover_list, contents_df, notes_df, table_1_df, table_2_df)
)
```

The function will return errors or warnings if anything is missing or seems odd. For example, we were warned that a value we supplied to `tab_title` had to be cleaned from 'Table 1' to 'Table_1', since blank spaces are not allowed in tab names. Note that there will be an error if there are any tab titles that start with a numeral.

Here's a preview of the object that was created:

```{r aftable-preview}
my_aftable
```

You can immediately tell that this is an aftable because it's the first word that's printed with the output.

So our aftable is basically just a table with one row per sheet and one column per sheet element. In fact, it has class 'data.frame'/'tbl' along with 'aftable'. For convenience, you can also check for the aftable class with `is_aftable()`.

Note that `create_aftable()` is the preferred method for generating aftable-class objects, but it's also possible to convert a correctly-formatted, pre-built data.frame or tibble directly to an aftable with `as_aftable()`.

### 2. Convert to a workbook

We can use `generate_workbook()` to convert our aftable to an [{openxlsx2}](https://janmarvin.github.io/openxlsx2/) wbWorkbook-class object.

This type of object adds information from each row of our aftable into separate sheets and applies other relevant structure, mark-up and styles for compliance with [the best practice guidance](https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/).

```{r create-af-wb}
my_wb <- aftables::generate_workbook(my_aftable)
```

<details><summary>Click for a preview of the Workbook object</summary>

The print method for a Workbook-class object is fairly limited, but you can see an overview of our named sheets and some of the custom styling.

```{r wb-preview}
my_wb
```
</details>

### 3. Write to file

Finally, you can use [the `wb_save()` function from {openxlsx2}](https://janmarvin.github.io/openxlsx2/reference/wb_save.html) to write your workbook object to an xlsx file (set the path argument to a location suitable for your work).

```{r saveworkbook, eval=FALSE}
openxlsx2::wb_save(wb = my_wb, file = "C://Documents//publication.xlsx")
```

You could also open a temporary copy of the workbook with `my_wb$open()`, which is convenient during the development process.

#### Output

The content of your output spreadsheet will end up looking something like this:

<div class="figure">
<img src="../man/figures/vignette-workbook.gif" alt="Gif of a simple Excel workbook created using the aftables package, revealing tabs for cover, contents, notes and tables." width="100%"/>
</div>

You'll notice that various best-practice formatting (e.g. Arial size 12 font for body text) and mark-up (e.g. tables, donated by a marker in the lower-right corner of the lower-right cell of each one) have been applied throughout.

Note also that two 'pre-table' meta-elements were created automatically in the sheets that contain statistical tables, which you didn't need to supply to `create_aftable()`: (1) the number of tables and (2) the presence of notes. These are required for accessibility good practice.

#### Final tweaks

It's your responsibility to check and amend the output from {aftables} to ensure it meets users' accessibility needs.

You can apply some final tweaks to the output xlsx file if the defaults don't quite meet your requirements (e.g. some column widths), though it's advisable to keep changes to a minimum for reproducibility purposes and because you may undo some of the compliant structuring and formatting that {aftables} provides.

You will need to manually address some other [accessibility requirements](https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/) that are not yet covered by the package such as converting the output to an ODS file rather than the proprietary xlsx format.

We're trying to address a number of these limitations. Please [see the issues on GitHub](https://github.com/best-practice-and-impact/aftables/issues) for the current status.

## Contribute

To contribute, please add [an issue](https://github.com/best-practice-and-impact/aftables/issues) or [a pull request](https://github.com/best-practice-and-impact/aftables/pulls) after reading [the code of conduct](https://github.com/best-practice-and-impact/aftables/blob/main/.github/CODE_OF_CONDUCT.md) and [contributing](https://github.com/best-practice-and-impact/aftables/blob/main/.github/CONTRIBUTING.md) guidance. 
