Skip to contents

This article is for people who want to contribute to ltsummary, or who want to understand the object well enough to modify it for their own needs. For an introduction to the package, start with the tbl_summary() tutorial.

The internal structures are documented for information. They may change, and a change to them is not treated as a breaking change.

Introduction

Every ltsummary table shares a few characteristics, and the object mirrors gtsummary’s on purpose: code that reads x$table_body from a gtsummary table reads an ltsummary table unchanged. The example table below is used throughout.

library(ltsummary)

tbl_summary_ex <- trial |>
  tbl_summary(by = trt, include = c(age, grade, response)) |>
  add_p() |>
  bold_labels()

Structure of an ltsummary object

Every ltsummary object is a list with, at minimum, these elements:

.$table_body    .$table_styling

A table built by tbl_summary() also carries .$stats, .$df_by, .$inputs and .$call_list.

names(tbl_summary_ex)
#> [1] "table_body"    "table_styling" "stats"         "df_by"        
#> [5] "inputs"        "call_list"

A table built by tbl_stack(), tbl_merge() or the tbl_strata() family carries .$tbls, the list of tables it was built from, and tbl_strata() adds .$df_strata recording the strata and their headers.

table_body

.$table_body is the data frame that is printed as the output. To work with every function of the package (the selectors in particular), it must include the columns "label", "row_type" and "variable". The "label" column is printed and the other two are hidden.

tbl_summary_ex$table_body
#>   variable             test_name    var_type      var_label row_type
#> 1      age           wilcox.test  continuous            Age    label
#> 2      age           wilcox.test  continuous            Age  missing
#> 3    grade chisq.test.no.correct categorical          Grade    label
#> 4    grade chisq.test.no.correct categorical          Grade    level
#> 5    grade chisq.test.no.correct categorical          Grade    level
#> 6    grade chisq.test.no.correct categorical          Grade    level
#> 7 response chisq.test.no.correct dichotomous Tumor Response    label
#> 8 response chisq.test.no.correct dichotomous Tumor Response  missing
#>            label      stat_1      stat_2  estimate parameter    statistic
#> 1            Age 59 (51, 66) 62 (54, 71) -3.000035        NA 3.699000e+03
#> 2        Unknown           4           7        NA        NA           NA
#> 3          Grade        <NA>        <NA>        NA         2 2.391539e+00
#> 4              I    25 (26%)    37 (35%)        NA        NA           NA
#> 5             II    40 (42%)    43 (41%)        NA        NA           NA
#> 6            III    30 (32%)    25 (24%)        NA        NA           NA
#> 7 Tumor Response    22 (24%)    24 (24%)        NA         1 3.142673e-03
#> 8        Unknown           2           5        NA        NA           NA
#>    conf.low    conf.high    p.value
#> 1 -6.999936 -4.69388e-05 0.04315754
#> 2        NA           NA         NA
#> 3        NA           NA 0.30247116
#> 4        NA           NA         NA
#> 5        NA           NA         NA
#> 6        NA           NA         NA
#> 7        NA           NA 0.95529437
#> 8        NA           NA         NA

The p.value, statistic, estimate and confidence interval columns are numeric; they are formatted when the table is printed, following the instructions in .$table_styling.

Combining tables adds hidden columns with reserved names. tbl_stack() (and tbl_uvregression(), which is built on it) adds tbl_id1 holding the position of the table each row came from, and a second stacking adds tbl_id2 outside it; group_header adds groupname_col, whose values become the group rows; and tbl_strata_nested_stack() adds tbl_indent_id1, which is 0 for the rows of a stratum’s own table and the nesting depth for the row headers above them. They are ordinary columns of table_body, so a rows = predicate can select on them, which is how the row headers of a nested stack are styled:

modify_bold(x, columns = label, rows = tbl_indent_id1 > 0)

table_styling

.$table_styling is a list of data frames describing how .$table_body is printed, formatted and styled. The data frames are header, spanning_header, footnote_header, footnote_body, footnote_spanning_header, abbreviation, source_note, text_format, indent, fmt_missing, fmt_fun, cols_merge and post_fmt_fun; caption and horizontal_line_above are single values. Apart from header, every data frame is append-only: each modify_*() call adds rows, and the rows are resolved in order when the table is printed, so a later instruction for the same cells wins.

names(tbl_summary_ex$table_styling)
#>  [1] "header"                   "spanning_header"         
#>  [3] "footnote_header"          "footnote_body"           
#>  [5] "footnote_spanning_header" "abbreviation"            
#>  [7] "source_note"              "text_format"             
#>  [9] "indent"                   "fmt_missing"             
#> [11] "fmt_fun"                  "cols_merge"              
#> [13] "post_fmt_fun"             "caption"                 
#> [15] "horizontal_line_above"

header

One row per column of .$table_body, holding the styling that applies to the whole column or its header.

Column Description
column column name from .$table_body
hide logical, whether the column is hidden in the output
align alignment of the column, "left", "right" or "center"
interpret_label how the label is interpreted, "md", "html" or "none"
label header label displayed when the column is shown
modify_stat_* statistics available to modify_header() and friends: modify_stat_level, modify_stat_N, modify_stat_n, modify_stat_p, modify_stat_N_event for regression tables, and modify_stat_time or modify_stat_prob for survival tables
tbl_summary_ex$table_styling$header
#>       column  hide  align interpret_label                 label
#> 1   variable  TRUE center              md              variable
#> 2  test_name  TRUE center              md             test_name
#> 3   var_type  TRUE center              md              var_type
#> 4  var_label  TRUE center              md             var_label
#> 5   row_type  TRUE center              md              row_type
#> 6      label FALSE   left              md    **Characteristic**
#> 7     stat_1 FALSE center              md  **Drug A**  \nN = 95
#> 8     stat_2 FALSE center              md **Drug B**  \nN = 105
#> 9   estimate  TRUE center              md        **Difference**
#> 10 parameter  TRUE center              md         **Parameter**
#> 11 statistic  TRUE center              md         **Statistic**
#> 12  conf.low  TRUE center              md            **95% CI**
#> 13 conf.high  TRUE center              md             conf.high
#> 14   p.value FALSE center              md           **p-value**
#>    modify_stat_level modify_stat_N modify_stat_n modify_stat_p
#> 1               <NA>           200            NA            NA
#> 2               <NA>           200            NA            NA
#> 3               <NA>           200            NA            NA
#> 4               <NA>           200            NA            NA
#> 5               <NA>           200            NA            NA
#> 6               <NA>           200            NA            NA
#> 7             Drug A           200            95         0.475
#> 8             Drug B           200           105         0.525
#> 9               <NA>           200            NA            NA
#> 10              <NA>           200            NA            NA
#> 11              <NA>           200            NA            NA
#> 12              <NA>           200            NA            NA
#> 13              <NA>           200            NA            NA
#> 14              <NA>           200            NA            NA
#>    modify_stat_N_event
#> 1                   NA
#> 2                   NA
#> 3                   NA
#> 4                   NA
#> 5                   NA
#> 6                   NA
#> 7                   NA
#> 8                   NA
#> 9                   NA
#> 10                  NA
#> 11                  NA
#> 12                  NA
#> 13                  NA
#> 14                  NA

spanning_header

Spanning headers, one row per column covered. level is the row of spanning headers, 1 being closest to the column headers; the current lt release renders level 1 only.

Column Description
level integer, the level of the spanning header
column column name from .$table_body
spanning_header the spanning header text
text_interpret how the text is interpreted, "md", "html" or "none"
remove logical, whether the spanning header is removed from the column

footnote_header, footnote_body and footnote_spanning_header

Footnotes on column headers, on body cells and on spanning headers. footnote_body has a rows column, and footnote_spanning_header a level column.

Column Description
column column name from .$table_body
rows expression selecting rows of .$table_body (body footnotes only)
footnote the footnote text
text_interpret how the text is interpreted, "md", "html" or "none"
replace logical, whether the footnote replaces existing footnotes at the location (TRUE) or is added to them (FALSE)
remove logical, whether every footnote at the location is removed
tbl_summary_ex$table_styling$footnote_header
#>      column                                           footnote text_interpret
#> 1    stat_1                             Median (Q1, Q3); n (%)             md
#> 2    stat_2                             Median (Q1, Q3); n (%)             md
#> 3   p.value Wilcoxon rank sum test; Pearson's Chi-squared test             md
#> 4  estimate Wilcoxon rank sum test; Pearson's Chi-squared test             md
#> 5 parameter Wilcoxon rank sum test; Pearson's Chi-squared test             md
#> 6 statistic Wilcoxon rank sum test; Pearson's Chi-squared test             md
#> 7  conf.low Wilcoxon rank sum test; Pearson's Chi-squared test             md
#>   replace remove
#> 1    TRUE  FALSE
#> 2    TRUE  FALSE
#> 3    TRUE  FALSE
#> 4    TRUE  FALSE
#> 5    TRUE  FALSE
#> 6    TRUE  FALSE
#> 7    TRUE  FALSE

abbreviation

Abbreviations are added one at a time and coalesced into a single note when the table is rendered. The prefix and separators chosen in modify_abbreviation() are stored in .$table_styling$abbreviation_format.

Column Description
column optional column name from .$table_body; when present, the abbreviation is printed only when the column appears in the rendered table
abbreviation the abbreviation, e.g. "CI = Confidence Interval"
text_interpret how the text is interpreted, "md", "html" or "none"

source_note

Source notes are numbered in the order they are added, which is how remove_source_note() refers to them.

Column Description
id integer identifying the source note
source_note the source note text
text_interpret how the text is interpreted, "md", "html" or "none"
remove logical, whether the source note is removed

text_format

Bold and italic styling of cells.

Column Description
column column name from .$table_body
rows expression selecting rows of .$table_body
format_type "bold" or "italic"
undo_text_format logical, whether the formatting is removed rather than added
tbl_summary_ex$table_styling$text_format
#>   column         rows format_type undo_text_format
#> 1  label ~, row_t....        bold            FALSE

indent

Indentation of cells, in spaces. The level and missing rows of a tbl_summary() table are indented by 4 spaces in the label column.

Column Description
column column name from .$table_body
rows expression selecting rows of .$table_body
n_spaces integer, the number of spaces of indentation

fmt_missing

By default NA values are printed as blanks. Rows of fmt_missing replace them with a symbol in the selected cells.

Column Description
column column name from .$table_body
rows expression selecting rows of .$table_body
symbol string printed in place of NA

fmt_fun and post_fmt_fun

Numeric columns are formatted with the functions in fmt_fun. The functions in post_fmt_fun run afterwards on the formatted text.

Column Description
column column name from .$table_body
rows expression selecting rows of .$table_body
fmt_fun list of formatting functions
str(tbl_summary_ex$table_styling$fmt_fun)
#> 'data.frame':    3 obs. of  3 variables:
#>  $ column : chr  "p.value" "parameter" "statistic"
#>  $ rows   :List of 3
#>   ..$ : NULL
#>   ..$ : NULL
#>   ..$ : NULL
#>   ..- attr(*, "class")= chr "AsIs"
#>  $ fmt_fun:List of 3
#>   ..$ :function (x)  
#>   ..$ :function (x)  
#>   ..$ :function (x)  
#>   ..- attr(*, "class")= chr "AsIs"

cols_merge

Instructions for merging columns into a single column. The merged text replaces the column named in column, which is the first column of the pattern.

Column Description
column column name from .$table_body
rows expression selecting rows of .$table_body
pattern glue pattern directing how to merge the columns

caption

A string printed as the table title, with a "text_interpret" attribute of "md", "html" or "none".

horizontal_line_above

Reserved for an expression identifying a row above which a horizontal line is drawn. Not used by tbl_summary().

The rows column

Every rows column holds one-sided formulas. The expression is captured when the modify_*() function is called and evaluated in .$table_body when the table is printed, so an instruction such as rows = row_type == "label" still applies after rows are added or removed. resolve_rows() turns each formula into row numbers inside finalize_styling(), which also applies the last-wins rule and drops instructions for hidden columns.

tbl_summary_ex$table_styling$text_format$rows
#> [[1]]
#> ~row_type == "label"
#> <environment: base>

stats

In place of the .$cards element of a gtsummary object, a tbl_summary() table stores its computed statistics in .$stats, a list with one long data frame per function that computed something (tbl_summary, add_overall, add_n, add_p). The columns are the variable and level, the by level, the column of .$table_body the statistic feeds, the statistic name, label, value and formatted value, and any warning or error raised while computing it. inline_text() and add_stat_label() read this element.

head(tbl_summary_ex$stats$tbl_summary, 8)
#>   variable variable_level group_level column context stat_name    stat_label
#> 1      age           <NA>      Drug A stat_1 missing     N_obs      No. obs.
#> 2      age           <NA>      Drug A stat_1 missing    N_miss     N Missing
#> 3      age           <NA>      Drug A stat_1 missing N_nonmiss N Non-missing
#> 4      age           <NA>      Drug A stat_1 missing    p_miss     % Missing
#> 5      age           <NA>      Drug A stat_1 missing p_nonmiss % Non-missing
#> 6      age           <NA>      Drug A stat_1 summary    median        Median
#> 7      age           <NA>      Drug A stat_1 summary       p25            Q1
#> 8      age           <NA>      Drug A stat_1 summary       p75            Q3
#>           stat stat_fmt warning error
#> 1           95       95    <NA>  <NA>
#> 2            4        4    <NA>  <NA>
#> 3           91       91    <NA>  <NA>
#> 4 0.042105....      4.2    <NA>  <NA>
#> 5 0.957894....       96    <NA>  <NA>
#> 6           59       59    <NA>  <NA>
#> 7           51       51    <NA>  <NA>
#> 8           66       66    <NA>  <NA>

inputs and call_list

.$inputs holds the resolved arguments of tbl_summary(): the data, the by variable, and one named list per formula-list argument, with a value for every included variable. add_overall() uses it to rebuild the table without by. .$call_list records the calls made on the table, in order, which is how add_stat_label() knows it has already run.

names(tbl_summary_ex$inputs)
#>  [1] "data"         "by"           "include"      "label"        "statistic"   
#>  [6] "digits"       "type"         "value"        "missing"      "missing_text"
#> [11] "missing_stat" "sort"         "percent"      "env"
names(tbl_summary_ex$call_list)
#> [1] "tbl_summary" "add_p"       "bold_labels"

Constructing an ltsummary object

table_body

Start from a data frame with the columns "label", "row_type" and "variable". Only "label" is printed; "row_type" typically decides whether the label is indented, and "variable" is used by inline_text() and the selectors.

tbl_summary_ex$table_body[c("variable", "row_type", "label")]
#>   variable row_type          label
#> 1      age    label            Age
#> 2      age  missing        Unknown
#> 3    grade    label          Grade
#> 4    grade    level              I
#> 5    grade    level             II
#> 6    grade    level            III
#> 7 response    label Tumor Response
#> 8 response  missing        Unknown

The other columns are created by the author and are printed, or not, according to .$table_styling.

table_styling

A few functions help construct and modify the styling:

  1. as_ltsummary(table_body) takes a data frame and returns the skeleton of an ltsummary object, with the full table_styling list.

  2. update_table_styling() (internal) re-synchronizes table_styling with the columns of table_body after columns are added or removed. New columns are hidden by default.

  3. modify_table_styling() records printing instructions for one or more columns.

  4. modify_table_body() applies a function to table_body and runs update_table_styling() afterwards, so the styling stays valid.

A small regression-style table built from scratch:

df <- data.frame(
  variable = c("age", "grade", "grade", "grade"),
  row_type = c("label", "label", "level", "level"),
  label = c("Age", "Grade", "II", "III"),
  estimate = c(0.12, NA, 1.4, 2.1),
  p.value = c(0.03, NA, 0.2, 0.001)
)

as_ltsummary(df) |>
  modify_header(label = "**Characteristic**", estimate = "**Beta**", p.value = "**p-value**") |>
  modify_fmt_fun(estimate = label_style_sigfig(), p.value = label_style_pvalue()) |>
  modify_indent(columns = label, rows = row_type == "level") |>
  bold_labels()

Printing an ltsummary object

Every ltsummary object is printed by print.ltsummary(). The object is first converted to an lt table with as_lt(), which uses .$table_styling to build a list of lt calls executed on .$table_body, and the lt table is then printed like any other. as_lt(return_calls = TRUE) returns the calls.

as_lt(tbl_summary_ex, return_calls = TRUE)[c("lt", "lt_label")]
#> $lt
#> lt::lt(list(label = c("Age", "Unknown", "Grade", "I", "II", "III", 
#> "Tumor Response", "Unknown"), stat_1 = c("59 (51, 66)", "4", 
#> "", "25 (26%)", "40 (42%)", "30 (32%)", "22 (24%)", "2"), stat_2 = c("62 (54, 71)", 
#> "7", "", "37 (35%)", "43 (41%)", "25 (24%)", "24 (24%)", "5"), 
#>     p.value = c("0.043", "", "0.3", "", "", "", ">0.9", "")), 
#>     auto_format = FALSE, auto_label = FALSE)
#> 
#> $lt_label
#> lt::lt_label(tbl, label = "<strong>Characteristic</strong>", 
#>     stat_1 = "<strong>Drug A</strong><br>N = 95", stat_2 = "<strong>Drug B</strong><br>N = 105", 
#>     p.value = "<strong>p-value</strong>")

The conversion is the only place where the package calls lt, so a change in lt’s interface is absorbed in one file, R/as_lt.R.