Getting Started

The primary motivation behind {clinify} is to take the things that are great about the R packages {flextable} and {officer}, take the standard and complex pieces of formatting clinical tables for regulatory use, and simplify the tedious pieces. {flextable} and {officer} offer a huge range of capability for creating tables in R and rendering them to various formats. {flextable} makes formatting the table itself straightforward, while {officer} gives you lower level access to create documents like docx files and insert separate components as needed.

When working with clinical tables, the devil is in the details. Every organization tends to have its own bits of nuance, and the flexibility of each organization to deviate from those standards varies. In the R world, there have still been a number of features that have either made generating clinical tables with certain features very tedious, or in some cases not possible with the current tooling. With {clinify} we attempt to close that gap and give some quality-of-life features to programmers making these tables.

Basics

Let’s start at the beginning

library(clinify)
library(flextable)
library(officer)

ct <- clintable(mtcars)
print(ct)

mpg

cyl

disp

hp

drat

wt

qsec

vs

am

gear

carb

21.0

6

160.0

110

3.90

2.620

16.46

0

1

4

4

21.0

6

160.0

110

3.90

2.875

17.02

0

1

4

4

22.8

4

108.0

93

3.85

2.320

18.61

1

1

4

1

21.4

6

258.0

110

3.08

3.215

19.44

1

0

3

1

18.7

8

360.0

175

3.15

3.440

17.02

0

0

3

2

18.1

6

225.0

105

2.76

3.460

20.22

1

0

3

1

14.3

8

360.0

245

3.21

3.570

15.84

0

0

3

4

24.4

4

146.7

62

3.69

3.190

20.00

1

0

4

2

22.8

4

140.8

95

3.92

3.150

22.90

1

0

4

2

19.2

6

167.6

123

3.92

3.440

18.30

1

0

4

4

17.8

6

167.6

123

3.92

3.440

18.90

1

0

4

4

16.4

8

275.8

180

3.07

4.070

17.40

0

0

3

3

17.3

8

275.8

180

3.07

3.730

17.60

0

0

3

3

15.2

8

275.8

180

3.07

3.780

18.00

0

0

3

3

10.4

8

472.0

205

2.93

5.250

17.98

0

0

3

4

In {clinify}, a clintable itself is at it’s root a flextable object with some extra metadata attached to it. A core part of the design philosophy of {clinify} is to build off of {flextable} at its core, extending functionality so that flextable functions are still operable on a clintable object.

The table printed above is the foundation of a clintable object. What we see here is the print method of a clintable being used. Compared to flextable, the primary thing that has is the application of default styling. Organizations generally have specific style preferences for their outputs, such as font and font size, standard conventions for borders, page size and margins, etc. These are configuration in {clinify} using some standard options that will be explained in another vignette. The print() method respects these settings to allow you to interactively explore your table being formatted.

Titles and Footnotes

Let’s expand some features of the table.

ct <- clintable(mtcars) |>
  clin_add_titles(
    list(
      c("Left", "Center", "Right"),
      c("Just the middle")
    )
  ) |> 
  clin_add_footnotes(
      list(
        c(
          "Here's a footnote.", 
          format(Sys.time(), "%H:%M %A, %B %d, %Y")
        )
      )
    )

print(ct)

Left

Center

Right

Just the middle

mpg

cyl

disp

hp

drat

wt

qsec

vs

am

gear

carb

21.0

6

160.0

110

3.90

2.620

16.46

0

1

4

4

21.0

6

160.0

110

3.90

2.875

17.02

0

1

4

4

22.8

4

108.0

93

3.85

2.320

18.61

1

1

4

1

21.4

6

258.0

110

3.08

3.215

19.44

1

0

3

1

18.7

8

360.0

175

3.15

3.440

17.02

0

0

3

2

18.1

6

225.0

105

2.76

3.460

20.22

1

0

3

1

14.3

8

360.0

245

3.21

3.570

15.84

0

0

3

4

24.4

4

146.7

62

3.69

3.190

20.00

1

0

4

2

22.8

4

140.8

95

3.92

3.150

22.90

1

0

4

2

19.2

6

167.6

123

3.92

3.440

18.30

1

0

4

4

17.8

6

167.6

123

3.92

3.440

18.90

1

0

4

4

16.4

8

275.8

180

3.07

4.070

17.40

0

0

3

3

17.3

8

275.8

180

3.07

3.730

17.60

0

0

3

3

15.2

8

275.8

180

3.07

3.780

18.00

0

0

3

3

10.4

8

472.0

205

2.93

5.250

17.98

0

0

3

4

Here's a footnote.

20:59 Monday, March 10, 2025

Here we’ve added some titles and footnotes to the document. The functions clin_add_titles() and clin_add_footnotes() allow you to insert titles and footnotes into the clintable metadata. When the clintable is written to a document or printed, the titles are respected. When you’re printing interactively, the HTML that’s rendered allows you to see the titles and footnotes above and below the table as if you’re viewing an individual page. When writing to a docx file, the titles are placed in the header and the footnotes are placed into the footer.

In this example, by providing a list of character vectors, each element of the list is added as a new line. There are some broad assumptions being made:

Ultimately, the attached tables are converted to flextables. As such, you can create your own flextable and attach it to the header or footer using the ft option in clin_add_titles(). We also have the helper function new_title_footnote() that allows you to supply a list and generate the flextable so you can apply extra formatting as desired.

Pagination and Alternating Pages

Let’s look at a couple more functions.

dat <- mtcars
dat['page'] <- c(
  rep(1, 10),
  rep(2, 10),
  rep(3, 10),
  c(4, 4)
)
dat2 <- rbind(dat, dat)
dat2['groups1'] <- c(
  rep('a', 32),
  rep('b', 32)
)
dat2['groups2'] <- c(
  rep('1', 16),
  rep('2', 16),
  rep('1', 16),
  rep('2', 16)
)

# Create a basic table
ct <- clintable(dat2) |> 
  clin_page_by('page') |> 
  clin_group_by(c('groups1', 'groups2')) |> 
  clin_alt_pages(
    key_cols = c('mpg', 'cyl', 'hp'),
    col_groups = list(
      c('disp', 'drat', 'wt'),
      c('qsec', 'vs', 'am'),
      c('gear', 'carb')
    ) 
  ) |> 
  clin_col_widths(mpg = .2, cyl=.2, disp=.15, vs=.15) |>
  clin_add_titles(
    list(
      c("Left", "Center", "Right"),
      c("Just the middle")
    )
  ) |> 
  clin_add_footnotes(
      list(
        c(
          "Here's a footnote.", 
          format(Sys.time(), "%H:%M %A, %B %d, %Y")
        )
      )
    )

print(ct)

Left

Center

Right

Just the middle

a
1

mpg

cyl

hp

disp

drat

wt

21.0

6

110

160.0

3.90

2.620

21.0

6

110

160.0

3.90

2.875

22.8

4

93

108.0

3.85

2.320

21.4

6

110

258.0

3.08

3.215

18.7

8

175

360.0

3.15

3.440

18.1

6

105

225.0

2.76

3.460

14.3

8

245

360.0

3.21

3.570

24.4

4

62

146.7

3.69

3.190

22.8

4

95

140.8

3.92

3.150

19.2

6

123

167.6

3.92

3.440

Here's a footnote.

20:59 Monday, March 10, 2025

A number of new things have happened here. Let’s go through function by function.

First, we’ve used the function clin_page_by() manually specify how page breaks should be handled. This is a data driven function, so here we’ve specified to use the page variable from the dat2 dataframe. Each time this variable changes, a page break will be inserted. Note that this isn’t used for sorting, just inserting a change break.

Next, we’ve used the function clin_group_by(). This allows us to put by lines above the column headers using data from the input data frame. Similar to clin_page_by() each time this value changes, a new page will start. Just like the table data, these lines above the column headers will always reflect the data from within the variable. You can use as many group variable as you need here.

Next we’ve used clin_alt_pages(). This has been one of the most requested features we’ve seen since we originally developed the package {pharmaRTF}. This feature is designed to handle cases where the number of variables you need to present overflow the width of the page that you have available. The function works with rotating pages, so the same data rows are presented for each overflowing page, necessary, while the columns being presented change. After the first input of the clintable object, you have two parameters:

If we look at that function specifically:

  [...] |>
  clin_alt_pages(
    key_cols = c('mpg', 'cyl', 'hp'),
    col_groups = list(
      c('disp', 'drat', 'wt'),
      c('qsec', 'vs', 'am'),
      c('gear', 'carb')
    ) 
  )

In total there will be 3 alternating pages. The columns presented on each page will be:

To make things easier for the developer, while developing interactively the print method has been updated to print 3 pages as styled HTML into the viewer pane, where you can select the page of choice from a page selector below the table. When printing to a word document, pages are inserted in the proper order. The logic works as follows:

Column Headers and Widths

From the previous example, another {clinify} function we used was clin_col_width(). The goal of this function is simply to make setting your column widths for a table straightforward. By default, in {flextable} your column widths are based on a unit such as inches or centimeters. In clin_col_width() we allow you to use the proportion of the page that you’d like that column to fill. From the syntax above:

[...] |>
  clin_col_widths(mpg = .2, cyl=.2, disp=.15, vs=.15) |>
[...]

In this case, we’re saying that:

The rest of the columns will be spaced evenly based on the remaining space. The space that’s filled is based on default configurations for page width, which are configurable within your session. Furthermore, clin_col_width() adapts to alternating pages. The ratios given to key columns apply for each alternating page, and the proportions applied to the additional col_group variables adapt any remaining space to ensure a page fits the total page width.

One last tedious part of structuring any table is getting the table headers formatted correctly. There are a couple specific features, such as spanning headers, which can also be tricky to get right, especially in a semi-automated way. For this reason, we’ve added the function clin_column_headers() to make this process a bit easier. Let’s use iris as an example of a table to which we want to apply some spanning headers.

  clintable(iris) |> 
    clin_column_headers(
      Sepal.Length = c("Flowers", "Sepal", "Length"),  
      Sepal.Width = c("Flowers", "Sepal", "Width"),  
      Petal.Length = c("Petal", "Length"),  
      Petal.Width = c("Petal", "Width")
    )

Flowers

 

Sepal

Petal

Length

Width

Length

Width

5.1

3.5

1.4

0.2

setosa

4.9

3.0

1.4

0.2

setosa

4.7

3.2

1.3

0.2

setosa

4.6

3.1

1.5

0.2

setosa

5.0

3.6

1.4

0.2

setosa

5.4

3.9

1.7

0.4

setosa

4.6

3.4

1.4

0.3

setosa

5.0

3.4

1.5

0.2

setosa

4.4

2.9

1.4

0.2

setosa

4.9

3.1

1.5

0.1

setosa

5.4

3.7

1.5

0.2

setosa

4.8

3.4

1.6

0.2

setosa

4.8

3.0

1.4

0.1

setosa

4.3

3.0

1.1

0.1

setosa

5.8

4.0

1.2

0.2

setosa

The first parameter of clin_column_headers() will be the clintable object for which you want headers to apply. From there, use the column name to which you’re applying a header. The way this function works is to use a character element for row of headers you want to apply. So for example, if you need three rows of headers, you can use 3 elements for a single column. The elements go to their respective rows.

When using spanning headers, you’re also typically using cell merging so that a single string of text spans over multiple columns. To accomplish this, repeat the text that you want to merge and ensure that those elements are placed in the same row. Consider the example above:

[...]
      Sepal.Length = c("Flowers", "Sepal", "Length"),  
      Sepal.Width = c("Flowers", "Sepal", "Width"),  
[...]

For the Sepal variable, we have two spanning headers. One for “flowers”, and one for “Sepal”. These cells will be merged, and the bottom row contains “Length” and “Width” separately.

Another common way you may want to apply headers is by using your variable labels. By default, clintable will respect this. Furthermore, the same spanning can be achieved as well. Let’s consider another example:

  iris2 <- iris
  attr(iris2$Sepal.Length, 'label') <- "Flower||Sepal||Length"
  attr(iris2$Sepal.Width, 'label') <- "Flower||Sepal||Width"
  attr(iris2$Petal.Length, 'label') <- "Flower||Petal||Length"
  attr(iris2$Petal.Width, 'label') <- "Flower||Petal||Width"

  clintable(iris2) |> 
    align(align='center', part='header') |>
    align(align='center', part='body')

Flower

Sepal

Petal

Length

Width

Length

Width

5.1

3.5

1.4

0.2

setosa

4.9

3.0

1.4

0.2

setosa

4.7

3.2

1.3

0.2

setosa

4.6

3.1

1.5

0.2

setosa

5.0

3.6

1.4

0.2

setosa

5.4

3.9

1.7

0.4

setosa

4.6

3.4

1.4

0.3

setosa

5.0

3.4

1.5

0.2

setosa

4.4

2.9

1.4

0.2

setosa

4.9

3.1

1.5

0.1

setosa

5.4

3.7

1.5

0.2

setosa

4.8

3.4

1.6

0.2

setosa

4.8

3.0

1.4

0.1

setosa

4.3

3.0

1.1

0.1

setosa

5.8

4.0

1.2

0.2

setosa

The underlying logic of this example is exactly the same as using clin_column_headers(), and in fact clin_column_headers() is actually called by default. The difference is that here, to separate levels we use the delimiter ||. Note in this example how the spanning variable have also changed so that Flower stretches over all four Petal and Sepal columns.

Note that after headers are applied, additional styling can be done. In this case, we use the flextable::align() function to change the alignment on both the table body and column headers to center. Note that default styles are applied when the table is written out or printed, so these might potentially override some settings depending on how those functions are applied.

Writing to DOCX

While printing the table during development helps you with the development process, ultimately {clinify} lets you write the document to a docx file. This was a primary reason why we wanted to build on top of {flextable}; with {flextable} and {officer} we’re able to have a great amount of control over how things are written specifically written into the word document. We try to make this process rather seamless and comparable to the print() method. To write the document out to docx, use the write_clintable() function. Let’s revisit our table from before.

# Create a basic table
ct <- clintable(dat2) |> 
  clin_page_by(max_rows = 36) |> 
  clin_group_by(c('groups1', 'groups2')) |> 
  clin_alt_pages(
    key_cols = c('mpg', 'cyl', 'hp'),
    col_groups = list(
      c('disp', 'drat', 'wt'),
      c('qsec', 'vs', 'am'),
      c('gear', 'carb')
    ) 
  ) |> 
  clin_col_widths(mpg = .2, cyl=.2, disp=.15, vs=.15) |>
  clin_add_titles(
    list(
      c("Left", "Center", "Right"),
      c("Just the middle")
    )
  ) |> 
  clin_add_footnotes(
      list(
        c(
          "Here's a footnote.", 
          format(Sys.time(), "%H:%M %A, %B %d, %Y")
        )
      )
    )

write_clintable(ct, file="example_table.docx")

Why this framework?

So why {clinify}? As we explained earlier, the key idea is that {flextable} and {officer} have so much of the functionality that’s already needed - so {clinify} focuses on specific additional features and streamline certain pieces to make the development of tables more straightforward. One last point is that in building this package, we also didn’t want to reinvent the wheel. Several other packages coordinate with the {flextable} and {officer} ecosystem, such as {rtables} or {gtsummary}. Our intent is that {clinify} can hopefully work with these packages as well introduce some of this tedious additional functionality.