This vignette is aimed at developers who want to understand the package better and to make it easier for them to contribute.
There are only two main user-facing functions in {a11ytables}:
create_a11ytable()
to create a data.frame object (with
an additional ‘a11ytable’ S3 class) filled with all the information
needed to create a spreadsheet output, as well as check the validity of
the structure and provide errors or warningsgenerate_workbook()
to convert the output from
create_a11ytable()
to an {openxlsx} Workbook-class
object, ready for writing to an xlsx file with
openxlsx::saveWorkbook()
This simplicity is a feature, not a bug. It’s designed to greatly simplify the process of creating compliant spreadsheets. The package does the hard work of making the outputs compliant so the user spends less time dealing with it.
This vignette provides a quick look at what’s happening ‘under the hood’ in these functions.
Please add an issue to the package’s GitHub repository if you would like any of this explanation to be expanded, or provide a solution in a pull request.
First it’s worth explaining how the source files are laid out. There
are four major groups of scripts in the R/
directory of the
package:
a11ytable.R
and
utils-a11ytable.R
contain code for handling the a11ytable
class, most importantly the create_a11ytable()
function,
but also coercion with as_a11ytable()
, checking with
is_a11ytable()
, a summary()
method and a
print()
method, which takes advantage of the {pillar} package for
prettier outputs.workbook.R
,
utils-workbook.R
and utils-workbook-style.R
contain the code for creating and styling a Workbook-class object with
the generate_workbook()
function.data.R
contains the
documentation for demo datasets, which are created in the
data-raw/
directory with the files stored in the
data/
directory.addin.R
and
utils-addin.R
contain code for the RStudio Addin (the
.dcf file for which is in the inst/rstudio/
directory).You’ll also find the a11ytables-package.R
file in the
R/
directory, which provides a package-level help page
derived from the DESCRIPTION file when ?a11ytables
is run
by the user. It doesn’t need to be edited.
This sections below focus on the create_a11ytable()
and
generate_workbook()
functions, which are the primary and
most complex functions in the package.
The code that underpins these functions is modularised to aid with bug-catching and testing, but also to make it easier for developers to understand how the code fits together. Internal sub-functions are consistently-named and begin with verbs, which should help you better understand their purpose.
Note that {a11ytables} uses a convention that internal functions
(i.e. those not presented to the user, but accessed via the
:::
qualifier) are prefixed with a period
(i.e. .f()
) to make it clearer that they are internal to
the package. The exported user-facing functions do not use a leading
period.
Actually, create_a11ytable()
itself only does one thing:
it takes user inputs from the arguments and combines them into a
dataframe. It then passes this off to the most important function in the
package, as_a11ytable()
, which is responsible for coercing
the dataframe to a11ytable class and performing checks on its
content.
Basically, as_a11ytable()
creates an S3-class object
with classes ‘data.frame’ and ‘tbl’ (i.e. tibble) and an additional
‘a11ytable’ class.
library(a11ytables)
my_a11ytable <- as_a11ytable(demo_df)
class(my_a11ytable)
# [1] "a11ytable" "tbl" "data.frame"
The object can be manipulated like a ‘normal’ dataframe and—thanks to the {pillar} package and the tbl class—it can be printed in compact form without the need for the whole of the {tibble} package to be imported.
my_a11ytable
# # a11ytable: 5 x 7
# tab_title sheet_type sheet_title blank_cells source custom_rows table
# <chr> <chr> <chr> <chr> <chr> <list> <list>
# 1 Cover cover The 'a11ytab… <NA> <NA> <chr [1]> <named list>
# 2 Contents contents Table of con… <NA> <NA> <chr [1]> <df [3 × 2]>
# 3 Notes notes Notes <NA> <NA> <chr [1]> <df [3 × 2]>
# 4 Table_1 tables Table_1: Fir… Blank cell… [The … <chr [2]> <df>
# 5 Table_2 tables Table_2: Sec… <NA> The S… <chr [1]> <df>
Compare this to its appearance as a regular data.frame, which is trickier to understand:
as.data.frame(my_a11ytable)
# tab_title sheet_type sheet_title
# 1 Cover cover The 'a11ytables' Demo Workbook
# 2 Contents contents Table of contents
# 3 Notes notes Notes
# 4 Table_1 tables Table_1: First Example Sheet
# 5 Table_2 tables Table_2: Second Example Sheet
# blank_cells
# 1 <NA>
# 2 <NA>
# 3 <NA>
# 4 Blank cells indicate that there's no note in that row.
# 5 <NA>
# source
# 1 <NA>
# 2 <NA>
# 3 <NA>
# 4 [The Source Material, 2024.](https://co-analysis.github.io/a11ytables/)
# 5 The Source Material, 2024.
# custom_rows
# 1 NA
# 2 NA
# 3 A custom row.
# 4 First custom row [with a hyperlink.](https://co-analysis.github.io/a11ytables/), Second custom row.
# 5 A custom row.
# table
# 1 First row of Section 1., Second row of Section 1., The only row of Section 2., [Website](https://co-analysis.github.io/a11ytables/), [Email address](mailto:[email protected])
# 2 Notes, Table 1, Table 2, Notes used in this workbook, First Example Sheet, Second Example Sheet
# 3 [note 1], [note 2], [note 3], First note., Second note., Third note.
# 4 A, B, C, D, E, F, G, H, I, J, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, [c], 6, 7, 8, 9, [x], 59180, 29260, 92130, 24000, 91970, 78650, 281050, 97720, 174630, 15230, 0.56094, 0.27176, 0.47238, 0.62704, 0.01278, 0.01788, 0.31304, 1.276, 0.54384, 0.81019, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, [note 2], NA, NA, NA, NA, [note 3], NA, NA, NA, NA
# 5 A, B, C, D, E, F, G, H, I, J, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Within as_a11ytable()
itself are two major functions
that help ensure proper construction of an a11ytable object:
.validate_a11ytable()
will generate errors if basic
structural expectations of an a11ytable aren’t met (e.g. if ‘cover’,
‘contents’ or ‘notes’ have been provided more than once to the
sheet_type
argument).warn_a11ytable()
checks for things that the user may
have forgotten and prints warnings about them (e.g. if 5 notes are
declared in the notes sheet but there are fewer in the tables
themselves)Advanced users can create a correctly-formatted data.frame on the fly
and convert it to an a11ytable with as_a11ytable()
directly. The as_a11ytable()
function mainly exists to make
testing easier, i.e. you can pass to it the pre-prepared
demo_df
dataset.
There’s a few methods for a11ytables that are also found in
R/a11ytables.R
.
is_a11ytable()
is a classic logical test that checks for
the a11ytable class in the object provided to it.
The summary()
method prints a very simple overview of a
provided a11ytable.
summary(my_a11ytable)
# # An a11ytable with 5 sheets:
# 1) Tab 'Cover' (sheet type 'cover') contains a list of length 3 (element lengths 2, 1 and 2)
# 2) Tab 'Contents' (sheet type 'contents') contains a 3 x 2 dataframe
# 3) Tab 'Notes' (sheet type 'notes') contains a 3 x 2 dataframe
# 4) Tab 'Table_1' (sheet type 'tables') contains a 10 x 7 dataframe
# 5) Tab 'Table_2' (sheet type 'tables') contains a 10 x 2 dataframe
The tbl_sum()
method is provided via the {pillar}
package, with the goal of providing a bespoke header to the printed
a11ytable.
The generate_workbook()
function sets up an {openxlsx} Workbook-class
object and fills it by iterating over a user-supplied the
a11ytable-class object.
my_wb <- generate_workbook(my_a11ytable)
class(my_wb)
# [1] "Workbook"
# attr(,"package")
# [1] "openxlsx"
You can see how the Workbook-class object carries information that will determine the structure and style of the final spreadsheet output.
my_wb
# A Workbook object.
#
# Worksheets:
# Sheet 1: "Cover"
#
# Custom row heights (row: height)
# 2: 34, 5: 34, 7: 34
# Custom column widths (column: width)
# 1: 72
#
#
# Sheet 2: "Contents"
#
# Custom column widths (column: width)
# 1: 16, 2: 56
#
#
# Sheet 3: "Notes"
#
# Custom column widths (column: width)
# 1: 16, 2: 56
#
#
# Sheet 4: "Table_1"
#
# Custom column widths (column: width)
# 1: 16, 2: 16, 3: 16, 4: 16, 5: 16, 6: 32, 7: 16
#
#
# Sheet 5: "Table_2"
#
# Custom column widths (column: width)
# 1: 16, 2: 16
#
#
#
# Worksheet write order: 1, 2, 3, 4, 5
# Active Sheet 1: "Cover"
# Position: 1
Several internal sub-functions within
generate_workbook()
—.add_*()
,
.insert_*()
and .style_*()
—are responsible for
adding these sheets, inserting sheet elements and styling them,
respectively.
A Workbook-class object is first created with
openxlsx::createWorkbook()
and then sheets are added based
on the contents of the user-supplied a11ytable.
The following functions add sheets and sheet elements into the workbook:
.add_tabs()
adds the required number of tabs into the
workbook with openxlsx::addWorksheet()
(as per the
tab_title
column of the supplied a11ytable).add_cover()
and .add_contents()
add the
information needed for the cover and contents sheets (as per the
required ‘cover’ and ‘contents’ supplied in the sheet_type
column of an a11ytable).add_notes()
if a notes sheet exists (i.e. a row in the
supplied a11ytable with a sheet_type
of ‘notes’).add_table()
adds sheets for each statistical table (as
per rows of supplied a11ytable with a sheet_type
of
‘table’)As sheets are added, content is inserted and styles are applied with the:
.insert_*()
functions, which insert sheet elements
(title, source statement, table, etc) to each sheet.style_*()
functions, which apply formatting to each
sheet (e.g. bold sheet titles with larger font) and the workbook
(e.g. Arial font)There are several .insert_*()
functions that add
information to each sheet depending on the sheet_type
of
the provided a11ytable, as well as the content, if any, of its
sheet_title
, blank_cells
, source
and table
columns.
The following functions insert ‘pre-table’ elements in this order:
.insert_title()
to place the sheet title in cell
A1.insert_table_count()
to add a statement about the
number of tables in the sheet.insert_notes_statement()
if a sheet_type
of ‘notes’ is provided in the user’s a11ytable.insert_blanks_message()
if content is provided in the
blanks_cells
column of the user’s a11ytable.insert_custom_rows()
if content is provided in the
custom_rows
column of the user’s a11ytable.insert_source()
if content is provided in the
source
column of the user’s a11ytableA table of data is added under the metadata with
.insert_table()
, which is provided in the
table
column of the user’s a11ytable object.
The exact .insert_*()
functions called depend on the
sheet_type
declared in the a11ytable:
.insert_title()
and .insert_table_count()
.insert_blanks_message()
,
.insert_custom_rows()
and .insert_source()
if
the relevant content is provided by the user, as well as
.insert_notes_statement()
if there are notesSimple logic is used to check for the presence of meta elements with
the .has_*()
functions, while the
get_start_row_*()
functions handle the cell to which each
message should be inserted.
For example, if all the elements are supplied, then the table would begin in row 6 (i.e. after the sheet title, table count, note presence, meaning of blank cells and source), but it’s possible that the table would have to be inserted to row 3 if only the sheet title and statement are required. This avoids inaccessible blank rows and redundant statements like ‘This table has no source statement’.
There are a few .style_*()
functions that create styles
and apply them on the basis of the sheet_type
provided in
the a11ytable.
.style_create()
creates an easily-referenced lookup of
styles, which is created with openxlsx::createStyle()
.style_workbook()
applies defaults for the whole
workbook (i.e. to set the font style to Arial size 12).style_cover()
, .style_contents()
and
.style_notes()
all apply styles to specific
sheets.style_sheet_title()
and .style_table()
apply styles to particular sheet elements (e.g. the title is
larger and bolder than the default font)To contribute, please add an issue or a pull request after reading the code of conduct and contributing guidance.