9 min read

Auto-summarising review points in Rmarkdown

The Overview

Rmarkdown documents have an R process running when they are compiled. We can use that to compute about the document when we compile it and have R make content that we didn’t write out long hand.

The Problem

I wanted a way to clearly highlight recommendations and have them summarised at the end of my document in a table but I only wanted to write them once and I didn’t want to have to manually collect them.

For about a year now, I’ve been using Rmarkdown documents at work whenever I write anything for internal distribution. We still use MS Word for our external reports (for now), but anything that I write that goes to someone else in the company is done in Rmarkdown.

One of the many advantages here is that the document is computable. By this, I mean that I can take advantage of the fact that there’s an R process running in the background when I compile to do some fun (and useful) things.

I review a lot of work carried out be other people. In an effort to provide both a clear record of my review process and to store recommendations for other people to follow, I like to produce a document outlining what I’ve looked at and what I think needs changing. This is almost always provided to the person who did the work in the first place as an html document.

I wanted a way to clearly highlight recommendations and have them summarised at the end of my document in a table but I only wanted to write them once and I didn’t want to have to manually collect them.

So my final requirements were:

  • I can easily mark recommendations in the flow of the text with minimal additional effort
  • The recommendations will be made obvious to the reader somehow without me doing anything extra at the time of writing
  • The recommendations will be collected and displayed at the bottom of the document in a table
  • The tabulation should be automatic: I should not have to gather up my recommendation points manually

Word might be able to do this, but if it can, it’s probably hidden deep in a menu somewhere. With computable documents, you can write a few lines of code and make this functionality for yourself.

My Solution

I used the fact that Rmarkdown documents are compiled in their own session. That means that I can (ab)use global assignment to avoid having to pass extra arguments to any functions without worrying about any other objects of the same name.

I first set up somewhere to hold all of my recommendations. I use a list, but it could also easily be a data frame. I set it up in the first code chunk of the Rmd, usually the setup chunk. As the list will hold points I want the reader to follow up on, I’m going to call the list follow_ups. For now it’s empty.

follow_ups <- list()

I want to make my follow up points stand out when displayed in the main body of the text. For my purposes, I’m happy just to set the colour in html, so I made a little helper function for that (which will work on any text we give it):

text_col <- function(text, colour) {
  glue::glue('<span style="color:{colour}">{text}</span>')
}

This uses the glue() function from {glue}, but you could make something just the same with paste(). All this function does it make some html that styles the text. If you do this sort of thing often or you want a significantly different style, you could use custom CSS To do so, you’d probably just to put your follow up point in a span with a particular class and style it all in the CSS stylesheet.

Now I’ve got a way to make the recommendation stand out, I’m going to need a way to add it to the list:

follow_up <- function(text) {
  follow_ups <<- append(text, follow_ups)
  
  text_col(text, "red")
}

This function uses the <<- assignment operator1. <<- is often thought of as a global assignment operator, but really it’s a little more subtle. The help pages explain it well:

The operators <<- and ->> are normally only used in functions, and cause a search to be made through parent environments for an existing definition of the variable being assigned. If such a variable is found (and its binding is not locked) then its value is redefined, otherwise assignment takes place in the global environment.

This assignment operator looks to assign to objects in parent environments up to the global environment, but if R can’t find a pre-existing object, it will make a new one in the global environment.

By using <<- here, I don’t have to pass around a copy of the follow_ups list. The one in the global environment of the Rmd will get updated.

Now I can write inline R code like: `r follow_up("look into this")` when I want to include a review point. This would appear in the document like: look into this.

I’m free to pepper these around my review document as much as a like, e.g. when I see something that I don’t follow: Explain this more to me. Or perhaps when something needs changing: This needs to change because of a reason and it should be documented.

Then right at the end, I will put a new section of the document called something like “Points to follow up” and in it I will run the following function (which is usually defined in the setup chunk).

show_follow_ups <- function() {
  data.frame(points = rev(unlist(follow_ups))) %>% 
    gt::gt() %>%
    gt::cols_label(points = "Points to follow up on")
    # knitr::kable()
}

This will produce a table with the follow up points all nicely listed like this.

show_follow_ups()
Points to follow up on
look into this
Explain this more to me
This needs to change because of a reason and it should be documented

Extensions

I’ve shown above the bare minimum of what is needed to set this up. At work I have expanded the functions in the following ways:

  • I’ve made follow_up() take two additional arguments, a context which only gets displayed by show_follow_ups(). This is for when I want to bring down some context into the recommendation table, but want to keep the recommendation itself succint in the main body. The second change in a resolution argument that allows me to record the action taken or the response to each review point. When the resolution is given, the original point is greyed out in the main body and the resolution text is displayed after it; in the table, the resolution text is shown in another column.
  • I’ve made follow_up() tag each recommendation point in the main body with an id in the <span> which I link to from the table at the bottom of the document. That way, if the context is not enough, the reader can click the link to go back up to the point in the body.

Conclusion

Having a clear list of review points makes my review notes clearer and easier to follow making my colleagues’ lives easier. Having R do all the work for me makes my life easier. Everyone wins.


  1. This is an example of why I prefer <- over = for assignment — it makes a consistently themed syntax for assignment↩︎